Index: src/org/openstreetmap/josm/actions/OpenLocationAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/OpenLocationAction.java	(revision 18170)
+++ src/org/openstreetmap/josm/actions/OpenLocationAction.java	(working copy)
@@ -43,7 +43,6 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
 import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
-import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Shortcut;
@@ -113,8 +112,7 @@
      * @param cbHistory the history combo box
      */
     protected void restoreUploadAddressHistory(HistoryComboBox cbHistory) {
-        cbHistory.setPossibleItemsTopDown(Config.getPref().getList(getClass().getName() + ".uploadAddressHistory",
-                Collections.emptyList()));
+        cbHistory.getModel().prefs().load(getClass().getName() + ".uploadAddressHistory");
     }
 
     /**
@@ -123,7 +121,7 @@
      */
     protected void remindUploadAddressHistory(HistoryComboBox cbHistory) {
         cbHistory.addCurrentItemToHistory();
-        Config.getPref().putList(getClass().getName() + ".uploadAddressHistory", cbHistory.getHistory());
+        cbHistory.getModel().prefs().save(getClass().getName() + ".uploadAddressHistory");
     }
 
     @Override
Index: src/org/openstreetmap/josm/actions/SearchNotesDownloadAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/SearchNotesDownloadAction.java	(revision 18170)
+++ src/org/openstreetmap/josm/actions/SearchNotesDownloadAction.java	(working copy)
@@ -7,7 +7,6 @@
 import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.util.Collections;
 import java.util.Optional;
 
 import javax.swing.JLabel;
@@ -44,7 +43,7 @@
     @Override
     public void actionPerformed(ActionEvent e) {
         HistoryComboBox searchTermBox = new HistoryComboBox();
-        searchTermBox.setPossibleItemsTopDown(Config.getPref().getList(HISTORY_KEY, Collections.emptyList()));
+        searchTermBox.getModel().prefs().load(HISTORY_KEY);
 
         JPanel contentPanel = new JPanel(new GridBagLayout());
         GridBagConstraints gc = new GridBagConstraints();
@@ -72,7 +71,7 @@
         }
 
         searchTermBox.addCurrentItemToHistory();
-        Config.getPref().putList(HISTORY_KEY, searchTermBox.getHistory());
+        searchTermBox.getModel().prefs().save(HISTORY_KEY);
 
         performSearch(searchTerm);
     }
Index: src/org/openstreetmap/josm/actions/UploadAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/UploadAction.java	(revision 18170)
+++ src/org/openstreetmap/josm/actions/UploadAction.java	(working copy)
@@ -239,7 +239,7 @@
         ChangesetUpdater.check();
 
         final UploadDialog dialog = UploadDialog.getUploadDialog();
-        dialog.setChangesetTags(layer.getDataSet());
+        dialog.initLifeCycle(layer.getDataSet());
         dialog.setUploadedPrimitives(apiData);
         dialog.setVisible(true);
         dialog.rememberUserInput();
Index: src/org/openstreetmap/josm/actions/search/SearchAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/search/SearchAction.java	(revision 18170)
+++ src/org/openstreetmap/josm/actions/search/SearchAction.java	(working copy)
@@ -13,11 +13,9 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 
 import javax.swing.JOptionPane;
 
@@ -42,6 +40,7 @@
 import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
 import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Shortcut;
@@ -66,7 +65,12 @@
 
     private static final String SEARCH_EXPRESSION = "searchExpression";
 
-    private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>();
+    private static AutoCompComboBoxModel<SearchSetting> model = new AutoCompComboBoxModel<>();
+
+    /** preferences reader/writer with automatic transmogrification to and from String */
+    private static AutoCompComboBoxModel<SearchSetting>.Preferences prefs = model.prefs(
+            SearchSetting::readFromString, SearchSetting::writeToString);
+
     static {
         SearchCompiler.addMatchFactory(new SimpleMatchFactory() {
             @Override
@@ -86,21 +90,15 @@
                 }
             }
         });
-
-        for (String s: Config.getPref().getList("search.history", Collections.<String>emptyList())) {
-            SearchSetting ss = SearchSetting.readFromString(s);
-            if (ss != null) {
-                searchHistory.add(ss);
-            }
-        }
+        model.setSize(Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE));
     }
 
     /**
      * Gets the search history
-     * @return The last searched terms. Do not modify it.
+     * @return The last searched terms.
      */
     public static Collection<SearchSetting> getSearchHistory() {
-        return searchHistory;
+        return model.asCollection();
     }
 
     /**
@@ -108,22 +106,8 @@
      * @param s The search to save
      */
     public static void saveToHistory(SearchSetting s) {
-        if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
-            searchHistory.addFirst(new SearchSetting(s));
-        } else if (searchHistory.contains(s)) {
-            // move existing entry to front, fixes #8032 - search history loses entries when re-using queries
-            searchHistory.remove(s);
-            searchHistory.addFirst(new SearchSetting(s));
-        }
-        int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
-        while (searchHistory.size() > maxsize) {
-            searchHistory.removeLast();
-        }
-        List<String> savedHistory = searchHistory.stream()
-                .map(SearchSetting::writeToString)
-                .distinct()
-                .collect(Collectors.toList());
-        Config.getPref().putList("search.history", savedHistory);
+        model.addTopElement(s);
+        prefs.save("search.history");
     }
 
     /**
@@ -131,9 +115,7 @@
      * @return The list of search texts.
      */
     public static List<String> getSearchExpressionHistory() {
-        return getSearchHistory().stream()
-                .map(ss -> ss.text)
-                .collect(Collectors.toList());
+        return prefs.asStringList();
     }
 
     private static volatile SearchSetting lastSearch;
@@ -175,7 +157,7 @@
         }
 
         SearchDialog dialog = new SearchDialog(
-                initialValues, getSearchExpressionHistory(), ExpertToggleAction.isExpert());
+                initialValues, model, ExpertToggleAction.isExpert());
 
         if (dialog.showDialog().getValue() != 1) return null;
 
@@ -203,6 +185,7 @@
      * Launches the dialog for specifying search criteria and runs a search
      */
     public static void search() {
+        prefs.load("search.history");
         SearchSetting se = showSearchDialog(lastSearch);
         if (se != null) {
             searchWithHistory(se);
Index: src/org/openstreetmap/josm/data/osm/search/SearchSetting.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/search/SearchSetting.java	(revision 18170)
+++ src/org/openstreetmap/josm/data/osm/search/SearchSetting.java	(working copy)
@@ -6,6 +6,7 @@
 import java.util.Objects;
 
 import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * This class defines a set of parameters that is used to
@@ -49,6 +50,16 @@
 
     @Override
     public String toString() {
+        return Utils.shortenString(text,
+            org.openstreetmap.josm.actions.search.SearchAction.MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
+    }
+
+    /**
+     * A more talkative version of toString.
+     * @return a bit more info than toString
+     * @since 18158
+     */
+    public String toStringEx() {
         String cs = caseSensitive ?
                 /*case sensitive*/  trc("search", "CS") :
                     /*case insensitive*/  trc("search", "CI");
@@ -139,6 +150,24 @@
     }
 
     /**
+     * Build a SearchSetting from a plain unformatted string.
+     * <p>
+     * All attributes are defaulted, only the search string is set. This function is used in
+     * {@link org.openstreetmap.josm.gui.download.OverpassQueryWizardDialog}.
+     *
+     * @param s The string
+     * @return The instance
+     * @since 18158
+     */
+    public static SearchSetting fromString(String s) {
+        if (s.isEmpty())
+            return null;
+        SearchSetting result = new SearchSetting();
+        result.text = s;
+        return result;
+    }
+
+    /**
      * Builds a string representation of the {@code SearchSetting} object,
      * see {@link #readFromString(String)} for more details.
      * @return A string representation of the {@code SearchSetting} object.
Index: src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItem.java
===================================================================
--- src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItem.java	(revision 18170)
+++ src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItem.java	(working copy)
@@ -94,32 +94,19 @@
     public boolean equals(Object obj) {
         if (this == obj)
             return true;
-        if (obj == null)
+        if (!(obj instanceof AutoCompletionItem))
             return false;
-        if (obj instanceof String)
-            return obj.equals(value);
-        if (getClass() != obj.getClass())
-            return false;
         final AutoCompletionItem other = (AutoCompletionItem) obj;
-        if (priority == null) {
-            if (other.priority != null)
-                return false;
-        } else if (!priority.equals(other.priority))
+        if (value == null ? other.value != null : !value.equals(other.value))
             return false;
-        if (value == null) {
-            if (other.value != null)
-                return false;
-        } else if (!value.equals(other.value))
-            return false;
-        return true;
+        return priority == null ? other.priority == null : priority.equals(other.priority);
     }
 
     @Override
     public int compareTo(AutoCompletionItem other) {
-        int ret = other.priority.compareTo(priority); // higher priority items come first in the list
-        if (ret != 0)
-            return ret;
-        else
-            return this.value.compareTo(other.value);
+        // sort on priority descending
+        int ret = other.priority.compareTo(priority);
+        // then alphabetic ascending
+        return ret != 0 ? ret : this.value.compareTo(other.value);
     }
 }
Index: src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java	(working copy)
@@ -178,8 +178,7 @@
      * @param cbHistory the {@link HistoryComboBox} to which the history is restored to
      */
     protected void restorePrimitivesHistory(HistoryComboBox cbHistory) {
-        cbHistory.setPossibleItemsTopDown(
-                Config.getPref().getList(getClass().getName() + ".primitivesHistory", Collections.emptyList()));
+        cbHistory.getModel().prefs().load(getClass().getName() + ".primitivesHistory");
     }
 
     /**
@@ -189,7 +188,7 @@
      */
     protected void remindPrimitivesHistory(HistoryComboBox cbHistory) {
         cbHistory.addCurrentItemToHistory();
-        Config.getPref().putList(getClass().getName() + ".primitivesHistory", cbHistory.getHistory());
+        cbHistory.getModel().prefs().save(getClass().getName() + ".primitivesHistory");
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/dialogs/SearchDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/SearchDialog.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/dialogs/SearchDialog.java	(working copy)
@@ -15,7 +15,6 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.util.Arrays;
-import java.util.List;
 
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
@@ -37,10 +36,11 @@
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSelector;
 import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
-import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.JosmRuntimeException;
 import org.openstreetmap.josm.tools.Logging;
@@ -54,7 +54,7 @@
 
     private final SearchSetting searchSettings;
 
-    protected final HistoryComboBox hcbSearchString = new HistoryComboBox();
+    protected final AutoCompComboBox<SearchSetting> hcbSearchString;
 
     private JCheckBox addOnToolbar;
     private JCheckBox caseSensitive;
@@ -71,12 +71,12 @@
     private TaggingPresetSelector selector;
     /**
      * Constructs a new {@code SearchDialog}.
-     * @param initialValues initial search settings
-     * @param searchExpressionHistory list of all texts that were recently used in the search
-     * @param expertMode expert mode
+     * @param initialValues initial search settings, eg. when opened for editing from the filter panel
+     * @param model The combobox model.
+     * @param expertMode expert mode. Shows more options and the "search syntax" panel.
      */
-    public SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, boolean expertMode) {
-        this(initialValues, searchExpressionHistory, new PanelOptions(expertMode, false), MainApplication.getMainFrame(),
+    public SearchDialog(SearchSetting initialValues, AutoCompComboBoxModel<SearchSetting> model, boolean expertMode) {
+        this(initialValues, model, new PanelOptions(expertMode, false), MainApplication.getMainFrame(),
                 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
                 initialValues instanceof Filter ? tr("Submit filter") : tr("Search"),
                 tr("Cancel"));
@@ -84,11 +84,12 @@
         configureContextsensitiveHelp("/Action/Search", true /* show help button */);
     }
 
-    protected SearchDialog(SearchSetting initialValues, List<String> searchExpressionHistory, PanelOptions options,
+    protected SearchDialog(SearchSetting initialValues, AutoCompComboBoxModel<SearchSetting> model, PanelOptions options,
                            Component mainFrame, String title, String... buttonTexts) {
         super(mainFrame, title, buttonTexts);
+        hcbSearchString = new AutoCompComboBox<>(model);
         this.searchSettings = new SearchSetting(initialValues);
-        setContent(buildPanel(searchExpressionHistory, options));
+        setContent(buildPanel(options));
     }
 
     /**
@@ -100,8 +101,8 @@
 
         /**
          * Constructs new options which determine which parts of the search dialog will be shown
-         * @param expertMode whether export mode is enabled
-         * @param overpassQuery whether the panel shall be adapted for Overpass query
+         * @param expertMode Shows more options and the "search syntax" panel.
+         * @param overpassQuery Don't show left panels and right "preset" panel. Show different "hints".
          */
         public PanelOptions(boolean expertMode, boolean overpassQuery) {
             this.expertMode = expertMode;
@@ -109,16 +110,15 @@
         }
     }
 
-    private JPanel buildPanel(List<String> searchExpressionHistory, PanelOptions options) {
+    private JPanel buildPanel(PanelOptions options) {
 
         // prepare the combo box with the search expressions
         JLabel label = new JLabel(searchSettings instanceof Filter ? tr("Filter string:") : tr("Search string:"));
 
         String tooltip = tr("Enter the search expression");
-        hcbSearchString.setText(searchSettings.text);
+        // FIXME do we need this?
+        hcbSearchString.setText(searchSettings.toString());
         hcbSearchString.setToolTipText(tooltip);
-
-        hcbSearchString.setPossibleItemsTopDown(searchExpressionHistory);
         hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height));
         label.setLabelFor(hcbSearchString);
 
@@ -306,7 +306,7 @@
         return addOnToolbar.isSelected();
     }
 
-    private static JPanel buildHintsSection(HistoryComboBox hcbSearchString, PanelOptions options) {
+    private static JPanel buildHintsSection(AutoCompComboBox<SearchSetting> hcbSearchString, PanelOptions options) {
         JPanel hintPanel = new JPanel(new GridBagLayout());
         hintPanel.setBorder(BorderFactory.createTitledBorder(tr("Hints")));
 
@@ -465,9 +465,9 @@
 
     private static class SearchKeywordRow extends JPanel {
 
-        private final HistoryComboBox hcb;
+        private final AutoCompComboBox<SearchSetting> hcb;
 
-        SearchKeywordRow(HistoryComboBox hcb) {
+        SearchKeywordRow(AutoCompComboBox<SearchSetting> hcb) {
             super(new FlowLayout(FlowLayout.LEFT));
             this.hcb = hcb;
         }
Index: src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(working copy)
@@ -87,7 +87,7 @@
 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.AutoCompletingComboBox;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
@@ -399,6 +399,29 @@
     }
 
     /**
+     * Returns the edited item with whitespaces removed
+     * @param cb the combobox
+     * @return the edited item with whitespaces removed
+     * @since 18158
+     */
+    public static String getEditItem(AutoCompComboBox<AutoCompletionItem> cb) {
+        return Utils.removeWhiteSpaces(cb.getEditor().getItem().toString());
+    }
+
+    /**
+     * Returns the selected item or the edited item as string
+     * @param cb the combobox
+     * @return the selected item or the edited item as string
+     * @since 18158
+     */
+    public static String getSelectedOrEditItem(AutoCompComboBox<AutoCompletionItem> cb) {
+        final Object selectedItem = cb.getSelectedItem();
+        if (selectedItem != null)
+            return selectedItem.toString();
+        return getEditItem(cb);
+    }
+
+    /**
      * Warns user about a key being overwritten.
      * @param action The action done by the user. Must state what key is changed
      * @param togglePref  The preference to save the checkbox state to
@@ -475,9 +498,11 @@
             AutoCompletionManager autocomplete = AutoCompletionManager.of(OsmDataManager.getInstance().getActiveDataSet());
             List<AutoCompletionItem> keyList = autocomplete.getTagKeys(DEFAULT_AC_ITEM_COMPARATOR);
 
-            keys = new AutoCompletingComboBox(key);
-            keys.setPossibleAcItems(keyList);
+            keys = new AutoCompComboBox<>();
+            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
+            keys.setPrototypeDisplayValue(new AutoCompletionItem(key));
             keys.setEditable(true);
+            keys.getModel().addAllElements(keyList);
             keys.setSelectedItem(key);
 
             p.add(Box.createVerticalStrut(5), GBC.eol());
@@ -489,13 +514,15 @@
 
             final String selection = m.size() != 1 ? tr("<different>") : m.entrySet().iterator().next().getKey();
 
-            values = new AutoCompletingComboBox(selection);
+            values = new AutoCompComboBox<>();
+            values.getModel().setComparator(Comparator.naturalOrder());
+            values.setPrototypeDisplayValue(new AutoCompletionItem(selection));
             values.setRenderer(cellRenderer);
-
             values.setEditable(true);
-            values.setPossibleAcItems(valueList);
+            values.getModel().addAllElements(valueList);
             values.setSelectedItem(selection);
             values.getEditor().setItem(selection);
+
             p.add(Box.createVerticalStrut(5), GBC.eol());
             p.add(new JLabel(tr("Value")), GBC.std());
             p.add(Box.createHorizontalStrut(10), GBC.std());
@@ -521,12 +548,12 @@
 
         @Override
         public void performTagEdit() {
-            String value = values.getEditItem();
+            String value = getEditItem(values);
             value = Normalizer.normalize(value, Normalizer.Form.NFC);
             if (value.isEmpty()) {
                 value = null; // delete the key
             }
-            String newkey = keys.getEditItem();
+            String newkey = getEditItem(keys);
             newkey = Normalizer.normalize(newkey, Normalizer.Form.NFC);
             if (newkey.isEmpty()) {
                 newkey = key;
@@ -573,8 +600,8 @@
     }
 
     protected abstract class AbstractTagsDialog extends ExtendedDialog {
-        protected AutoCompletingComboBox keys;
-        protected AutoCompletingComboBox values;
+        protected AutoCompComboBox<AutoCompletionItem> keys;
+        protected AutoCompComboBox<AutoCompletionItem> values;
 
         AbstractTagsDialog(Component parent, String title, String... buttonTexts) {
             super(parent, title, buttonTexts);
@@ -620,7 +647,7 @@
             super.setVisible(visible);
         }
 
-        private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) {
+        private void selectACComboBoxSavingUnixBuffer(AutoCompComboBox<AutoCompletionItem> cb) {
             // select combobox with saving unix system selection (middle mouse paste)
             Clipboard sysSel = ClipboardUtils.getSystemSelection();
             if (sysSel != null) {
@@ -666,7 +693,8 @@
                    boolean valuesOK = size == currentModel.getSize()
                            && IntStream.range(0, size).allMatch(i -> Objects.equals(currentModel.getElementAt(i), correctItems.get(i)));
                    if (!valuesOK) {
-                       values.setPossibleAcItems(correctItems);
+                       values.getModel().removeAllElements();
+                       values.getModel().addAllElements(correctItems);
                    }
                    if (!Objects.equals(key, objKey)) {
                        values.getEditor().selectAll();
@@ -687,7 +715,7 @@
             if (buttons.isEmpty()) {
                 return;
             }
-            buttons.get(0).setIcon(findIcon(keys.getSelectedOrEditItem(), values.getSelectedOrEditItem())
+            buttons.get(0).setIcon(findIcon(getSelectedOrEditItem(keys), getSelectedOrEditItem(values))
                     .orElse(ImageProvider.get("ok", ImageProvider.ImageSizes.LARGEICON)));
         }
 
@@ -731,8 +759,12 @@
             configureContextsensitiveHelp("/Dialog/AddValue", true /* show help button */);
 
             mainPanel = new JPanel(new GridBagLayout());
-            keys = new AutoCompletingComboBox();
-            values = new AutoCompletingComboBox();
+            keys = new AutoCompComboBox<>();
+            values = new AutoCompComboBox<>();
+            keys.getModel().setComparator(Comparator.naturalOrder()); // according to Comparable
+            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());
 
@@ -747,7 +779,8 @@
             // remove the object's tag keys from the list
             keyList.removeIf(item -> containsDataKey(item.getValue()));
 
-            keys.setPossibleAcItems(keyList);
+            keys.getModel().removeAllElements();
+            keys.getModel().addAllElements(keyList);
             keys.setEditable(true);
 
             mainPanel.add(keys, GBC.eop().fill(GBC.HORIZONTAL));
@@ -938,10 +971,10 @@
                         tr("Choose recent tag {0}", count), null, tr("Use this tag again"), sc, false) {
                     @Override
                     public void actionPerformed(ActionEvent e) {
-                        keys.setSelectedItem(t.getKey(), true);
+                        keys.setSelectedItem(t.getKey());
                         // fix #7951, #8298 - update list of values before setting value (?)
                         focus.focusGained(null);
-                        values.setSelectedItem(t.getValue(), true);
+                        values.setSelectedItem(t.getValue());
                         selectValuesCombobox();
                     }
                 };
@@ -1093,8 +1126,8 @@
          * Read tags from comboboxes and add it to all selected objects
          */
         public final void performTagAdding() {
-            String key = keys.getEditItem();
-            String value = values.getEditItem();
+            String key = getEditItem(keys);
+            String value = getEditItem(values);
             if (key.isEmpty() || value.isEmpty())
                 return;
             for (OsmPrimitive osm : sel) {
Index: src/org/openstreetmap/josm/gui/download/OverpassQueryWizardDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/download/OverpassQueryWizardDialog.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/download/OverpassQueryWizardDialog.java	(working copy)
@@ -14,6 +14,7 @@
 import org.openstreetmap.josm.data.preferences.ListProperty;
 import org.openstreetmap.josm.gui.dialogs.SearchDialog;
 import org.openstreetmap.josm.gui.download.overpass.OverpassWizardRegistration.OverpassWizardCallbacks;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.SearchCompilerQueryWizard;
 import org.openstreetmap.josm.tools.UncheckedParseException;
@@ -35,18 +36,26 @@
     private static final int BUILD_AN_EXECUTE_QUERY = 1;
     private static final int CANCEL = 2;
 
+    private AutoCompComboBoxModel<SearchSetting> model;
+
+    /** preferences reader/writer with automatic transmogrification to and from String */
+    private AutoCompComboBoxModel<SearchSetting>.Preferences prefs;
+
     /**
      * Create a new {@link OverpassQueryWizardDialog}
      * @param callbacks The Overpass download source panel.
      */
     public OverpassQueryWizardDialog(OverpassWizardCallbacks callbacks) {
-        super(new SearchSetting(), OVERPASS_WIZARD_HISTORY.get(), new PanelOptions(false, true), callbacks.getParent(),
+        super(new SearchSetting(), new AutoCompComboBoxModel<>(), new PanelOptions(false, true), callbacks.getParent(),
                 tr("Overpass Query Wizard"),
                 tr("Build query"), tr("Build query and execute"), tr("Cancel"));
         this.callbacks = callbacks;
+        model = hcbSearchString.getModel();
         setButtonIcons("dialogs/magic-wand", "download-overpass", "cancel");
         setCancelButton(CANCEL + 1);
         setDefaultButton(BUILD_AN_EXECUTE_QUERY + 1);
+        prefs = model.prefs(SearchSetting::fromString, SearchSetting::toString);
+        prefs.load(OVERPASS_WIZARD_HISTORY);
     }
 
     @Override
@@ -75,8 +84,8 @@
      * Saves the latest, successfully parsed search term.
      */
     private void saveHistory() {
-        hcbSearchString.addCurrentItemToHistory();
-        OVERPASS_WIZARD_HISTORY.put(hcbSearchString.getHistory());
+        hcbSearchString.getModel().addTopElement(SearchSetting.fromString(hcbSearchString.getText()));
+        prefs.save(OVERPASS_WIZARD_HISTORY);
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/download/PlaceSelection.java
===================================================================
--- src/org/openstreetmap/josm/gui/download/PlaceSelection.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/download/PlaceSelection.java	(working copy)
@@ -116,7 +116,7 @@
 
         cbSearchExpression = new HistoryComboBox();
         cbSearchExpression.setToolTipText(tr("Enter a place name to search for"));
-        cbSearchExpression.setPossibleItemsTopDown(Config.getPref().getList(HISTORY_KEY, Collections.emptyList()));
+        cbSearchExpression.getModel().prefs().load(HISTORY_KEY);
         lpanel.add(cbSearchExpression, GBC.std(1, 1).fill(GBC.HORIZONTAL));
 
         panel.add(lpanel, GBC.std().fill(GBC.HORIZONTAL).insets(5, 5, 0, 5));
@@ -194,7 +194,7 @@
             if (!isEnabled() || searchExpression.trim().isEmpty())
                 return;
             cbSearchExpression.addCurrentItemToHistory();
-            Config.getPref().putList(HISTORY_KEY, cbSearchExpression.getHistory());
+            cbSearchExpression.getModel().prefs().save(HISTORY_KEY);
             Server server = (Server) serverComboBox.getSelectedItem();
             URL url = server.urlFunction.apply(searchExpression, isSearchMore ? model.getData() : Collections.emptyList());
             NameQueryTask task = new NameQueryTask(url, data -> {
Index: src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanel.java	(working copy)
@@ -3,18 +3,22 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.awt.Component;
 import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
 import java.awt.event.ItemEvent;
-import java.awt.event.KeyAdapter;
+import java.awt.event.ItemListener;
 import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.Collection;
 import java.util.List;
-import java.util.Objects;
+import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 import javax.swing.BorderFactory;
@@ -23,14 +27,9 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
-import javax.swing.SwingUtilities;
-import javax.swing.event.AncestorEvent;
-import javax.swing.event.AncestorListener;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
 import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
 
 import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -38,6 +37,7 @@
 import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadAreaValidator;
 import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadCommentValidator;
 import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadSourceValidator;
+import org.openstreetmap.josm.gui.tagging.TagModel;
 import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
 import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -48,21 +48,21 @@
  * BasicUploadSettingsPanel allows to enter the basic parameters required for uploading data.
  * @since 2599
  */
-public class BasicUploadSettingsPanel extends JPanel {
+public class BasicUploadSettingsPanel extends JPanel implements ActionListener, FocusListener, ItemListener, KeyListener, TableModelListener {
     /**
-     * Preference name for history collection
+     * Preference name for the history of comments
      */
-    public static final String HISTORY_KEY = "upload.comment.history";
+    public static final String COMMENT_HISTORY_KEY = "upload.comment.history";
     /**
      * Preference name for last used upload comment
      */
-    public static final String HISTORY_LAST_USED_KEY = "upload.comment.last-used";
+    public static final String COMMENT_LAST_USED_KEY = "upload.comment.last-used";
     /**
      * Preference name for the max age search comments may have
      */
-    public static final String HISTORY_MAX_AGE_KEY = "upload.comment.max-age";
+    public static final String COMMENT_MAX_AGE_KEY = "upload.comment.max-age";
     /**
-     * Preference name for the history of source values
+     * Preference name for the history of sources
      */
     public static final String SOURCE_HISTORY_KEY = "upload.source.history";
 
@@ -78,9 +78,7 @@
     private final JLabel areaValidatorFeedback = new JLabel();
     private final UploadAreaValidator areaValidator = new UploadAreaValidator(new JTextField(), areaValidatorFeedback);
     /** the changeset comment model */
-    private final transient ChangesetCommentModel changesetCommentModel;
-    private final transient ChangesetCommentModel changesetSourceModel;
-    private final transient ChangesetReviewModel changesetReviewModel;
+    private final transient UploadDialogModel model;
     private final transient JLabel uploadCommentFeedback = new JLabel();
     private final transient UploadCommentValidator uploadCommentValidator = new UploadCommentValidator(
             hcbUploadComment.getEditorComponent(), uploadCommentFeedback);
@@ -88,6 +86,22 @@
     private final transient UploadSourceValidator uploadSourceValidator = new UploadSourceValidator(
             hcbUploadSource.getEditorComponent(), hcbUploadSourceFeedback);
 
+    /** a lock to prevent loops in notifications */
+    private boolean locked;
+
+    /**
+     * Creates the panel
+     *
+     * @param model The tag editor model.
+     *
+     * @since 18158 (signature)
+     */
+    public BasicUploadSettingsPanel(UploadDialogModel model) {
+        this.model = model;
+        this.model.addTableModelListener(this);
+        build();
+    }
+
     protected JPanel buildUploadCommentPanel() {
         JPanel pnl = new JPanel(new GridBagLayout());
         pnl.setBorder(BorderFactory.createTitledBorder(tr("Provide a brief comment for the changes you are uploading:")));
@@ -94,11 +108,11 @@
 
         hcbUploadComment.setToolTipText(tr("Enter an upload comment"));
         hcbUploadComment.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
-        populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<>());
-        CommentModelListener commentModelListener = new CommentModelListener(hcbUploadComment, changesetCommentModel);
-        hcbUploadComment.getEditor().addActionListener(commentModelListener);
-        hcbUploadComment.getEditorComponent().addFocusListener(commentModelListener);
-        hcbUploadComment.getEditorComponent().getDocument().addDocumentListener(commentModelListener);
+        JTextField editor = hcbUploadComment.getEditorComponent();
+        editor.getDocument().putProperty("tag", "comment");
+        editor.addKeyListener(this);
+        editor.addFocusListener(this);
+        editor.addActionListener(this);
         pnl.add(hcbUploadComment, GBC.eol().fill(GBC.HORIZONTAL));
         pnl.add(uploadCommentFeedback, GBC.eol().insets(0, 3, 0, 0).fill(GBC.HORIZONTAL));
         return pnl;
@@ -112,14 +126,15 @@
                 "<html>(<a href=\"urn:changeset-source\">" + tr("just once") + "</a>)</html>");
         obtainSourceOnce.addHyperlinkListener(e -> {
             if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
-                automaticallyAddSource();
+                saveEdits();
+                model.put("source", getSourceFromLayer());
             }
         });
         obtainSourceAutomatically.setSelected(Config.getPref().getBoolean("upload.source.obtainautomatically", false));
         obtainSourceAutomatically.addActionListener(e -> {
-            if (obtainSourceAutomatically.isSelected())
-                automaticallyAddSource();
-
+            if (obtainSourceAutomatically.isSelected()) {
+                model.put("source", getSourceFromLayer());
+            }
             obtainSourceOnce.setVisible(!obtainSourceAutomatically.isSelected());
         });
         JPanel obtainSource = new JPanel(new GridBagLayout());
@@ -132,65 +147,75 @@
 
         hcbUploadSource.setToolTipText(tr("Enter a source"));
         hcbUploadSource.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
-        populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources());
-        CommentModelListener sourceModelListener = new CommentModelListener(hcbUploadSource, changesetSourceModel);
-        hcbUploadSource.getEditor().addActionListener(sourceModelListener);
-        hcbUploadSource.getEditorComponent().addFocusListener(sourceModelListener);
-        hcbUploadSource.getEditorComponent().getDocument().addDocumentListener(sourceModelListener);
+        JTextField editor = hcbUploadSource.getEditorComponent();
+        editor.getDocument().putProperty("tag", "source");
+        editor.addKeyListener(this);
+        editor.addFocusListener(this);
+        editor.addActionListener(this);
         pnl.add(hcbUploadSource, GBC.eol().fill(GBC.HORIZONTAL));
         pnl.add(hcbUploadSourceFeedback, GBC.eol().insets(0, 3, 0, 0).fill(GBC.HORIZONTAL));
-        if (obtainSourceAutomatically.isSelected()) {
-            automaticallyAddSource();
-        }
-        pnl.addAncestorListener(new AncestorListener() {
-            @Override
-            public void ancestorAdded(AncestorEvent event) {
-                if (obtainSourceAutomatically.isSelected())
-                    automaticallyAddSource();
-            }
 
-            @Override
-            public void ancestorRemoved(AncestorEvent event) {
-                // Do nothing
-            }
-
-            @Override
-            public void ancestorMoved(AncestorEvent event) {
-                // Do nothing
-            }
-        });
         return pnl;
     }
 
     /**
-     * Add the source tags
+     * Initializes this life cycle of the panel.
+     *
+     * Adds any changeset tags to the map.
+     *
+     * @param map Map where tags are added to.
+     * @since 18158
      */
-    protected void automaticallyAddSource() {
-        final String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag();
-        hcbUploadSource.getModel().setSelectedItem(null); // fix #20134
-        hcbUploadSource.setText(Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH));
-        changesetSourceModel.setComment(hcbUploadSource.getText()); // Fix #9965
+    public void initLifeCycle(Map<String, String> map) {
+        Optional.ofNullable(getLastChangesetTagFromHistory(COMMENT_HISTORY_KEY, new ArrayList<>())).ifPresent(
+                x -> map.put("comment", x));
+        Optional.ofNullable(getLastChangesetTagFromHistory(SOURCE_HISTORY_KEY, getDefaultSources())).ifPresent(
+                x -> map.put("source", x));
+        if (obtainSourceAutomatically.isSelected()) {
+            map.put("source", getSourceFromLayer());
+        }
+        hcbUploadComment.getModel().prefs().load(COMMENT_HISTORY_KEY);
+        hcbUploadComment.discardAllUndoableEdits();
+        hcbUploadSource.getModel().prefs().load(SOURCE_HISTORY_KEY, getDefaultSources());
+        hcbUploadSource.discardAllUndoableEdits();
     }
 
     /**
-     * Refreshes contents of upload history combo boxes from preferences.
+     * Get a key's value from the model.
+     * @param key The key
+     * @return The value or ""
+     * @since 18158
      */
-    protected void refreshHistoryComboBoxes() {
-        populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<>());
-        populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources());
+    private String get(String key) {
+        TagModel tm = model.get(key);
+        return tm == null ? "" : tm.getValue();
     }
 
-    private static void populateHistoryComboBox(HistoryComboBox hcb, String historyKey, List<String> defaultValues) {
-        hcb.setPossibleItemsTopDown(Config.getPref().getList(historyKey, defaultValues));
-        hcb.discardAllUndoableEdits();
+    /**
+     * Get the topmost item from the history if not expired.
+     *
+     * @param historyKey The preferences key.
+     * @param def A default history.
+     * @return The history item (may be null).
+     * @since 18158 (signature)
+     */
+    public static String getLastChangesetTagFromHistory(String historyKey, List<String> def) {
+        Collection<String> history = Config.getPref().getList(historyKey, def);
+        long age = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - getHistoryLastUsedKey();
+        if (age < getHistoryMaxAgeKey() && !history.isEmpty()) {
+            return history.iterator().next();
+        }
+        return null;
     }
 
     /**
-     * Discards undoable edits of upload history combo boxes.
+     * Add the "source" tag
+     * @return The source from the layer info.
+     * @since xxx
      */
-    protected void discardAllUndoableEdits() {
-        hcbUploadComment.discardAllUndoableEdits();
-        hcbUploadSource.discardAllUndoableEdits();
+    private String getSourceFromLayer() {
+        String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag();
+        return Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH);
     }
 
     /**
@@ -219,7 +244,7 @@
         add(pnlUploadParameterSummary, gbc);
         if (Config.getPref().getBoolean("upload.show.review.request", true)) {
             add(cbRequestReview, gbc);
-            cbRequestReview.addItemListener(e -> changesetReviewModel.setReviewRequested(e.getStateChange() == ItemEvent.SELECTED));
+            cbRequestReview.addItemListener(this);
         }
         add(areaValidatorFeedback, gbc);
         add(new JPanel(), GBC.std().fill(GBC.BOTH));
@@ -226,36 +251,6 @@
     }
 
     /**
-     * Creates the panel
-     *
-     * @param changesetCommentModel the model for the changeset comment. Must not be null
-     * @param changesetSourceModel the model for the changeset source. Must not be null.
-     * @param changesetReviewModel the model for the changeset review. Must not be null.
-     * @throws NullPointerException if a model is null
-     * @since 12719 (signature)
-     */
-    public BasicUploadSettingsPanel(ChangesetCommentModel changesetCommentModel, ChangesetCommentModel changesetSourceModel,
-            ChangesetReviewModel changesetReviewModel) {
-        this.changesetCommentModel = Objects.requireNonNull(changesetCommentModel, "changesetCommentModel");
-        this.changesetSourceModel = Objects.requireNonNull(changesetSourceModel, "changesetSourceModel");
-        this.changesetReviewModel = Objects.requireNonNull(changesetReviewModel, "changesetReviewModel");
-        changesetCommentModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadComment));
-        changesetSourceModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadSource));
-        changesetReviewModel.addChangeListener(new ChangesetReviewChangeListener());
-        build();
-    }
-
-    void setUploadTagDownFocusTraversalHandlers(final ActionListener handler) {
-        setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadComment);
-        setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadSource);
-    }
-
-    private static void setHistoryComboBoxDownFocusTraversalHandler(ActionListener handler, HistoryComboBox hcb) {
-        hcb.getEditor().addActionListener(handler);
-        hcb.getEditorComponent().addKeyListener(new HistoryComboBoxKeyAdapter(hcb, handler));
-    }
-
-    /**
      * Remembers the user input in the preference settings
      */
     public void rememberUserInput() {
@@ -262,12 +257,12 @@
         // store the history of comments
         if (getHistoryMaxAgeKey() > 0) {
             hcbUploadComment.addCurrentItemToHistory();
-            Config.getPref().putList(HISTORY_KEY, hcbUploadComment.getHistory());
-            Config.getPref().putLong(HISTORY_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
+            hcbUploadComment.getModel().prefs().save(COMMENT_HISTORY_KEY);
+            Config.getPref().putLong(COMMENT_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
         }
         // store the history of sources
         hcbUploadSource.addCurrentItemToHistory();
-        Config.getPref().putList(SOURCE_HISTORY_KEY, hcbUploadSource.getHistory());
+        hcbUploadSource.getModel().prefs().save(SOURCE_HISTORY_KEY);
 
         // store current value of obtaining source automatically
         Config.getPref().putBoolean("upload.source.obtainautomatically", obtainSourceAutomatically.isSelected());
@@ -277,7 +272,6 @@
      * Initializes the panel for user input
      */
     public void startUserInput() {
-        hcbUploadComment.requestFocusInWindow();
         hcbUploadComment.getEditorComponent().requestFocusInWindow();
         uploadCommentValidator.validate();
         uploadSourceValidator.validate();
@@ -311,123 +305,151 @@
         return pnlUploadParameterSummary;
     }
 
-    /**
-     * Forces update of comment/source model if matching text field is focused.
-     * @since 14977
-     */
-    public void forceUpdateActiveField() {
-        updateModelIfFocused(hcbUploadComment, changesetCommentModel);
-        updateModelIfFocused(hcbUploadSource, changesetSourceModel);
-    }
-
-    private static void updateModelIfFocused(HistoryComboBox hcb, ChangesetCommentModel changesetModel) {
-        if (hcb.getEditorComponent().hasFocus()) {
-            changesetModel.setComment(hcb.getText());
-        }
-    }
-
     static long getHistoryMaxAgeKey() {
-        return Config.getPref().getLong(HISTORY_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4));
+        return Config.getPref().getLong(COMMENT_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4));
     }
 
     static long getHistoryLastUsedKey() {
-        return Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0);
+        return Config.getPref().getLong(COMMENT_LAST_USED_KEY, 0);
     }
 
-    static final class HistoryComboBoxKeyAdapter extends KeyAdapter {
-        private final HistoryComboBox hcb;
-        private final ActionListener handler;
-
-        HistoryComboBoxKeyAdapter(HistoryComboBox hcb, ActionListener handler) {
-            this.hcb = hcb;
-            this.handler = handler;
+    /**
+     * Updates the combobox histories when a combobox editor loses focus.
+     *
+     * @param text The {@code JTextField} of the combobox editor.
+     * @since xxx
+     */
+    private void updateHistory(JTextField text) {
+        String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source"
+        if (tag.equals("comment")) {
+            hcbUploadComment.addCurrentItemToHistory();
         }
+        if (tag.equals("source")) {
+            hcbUploadSource.addCurrentItemToHistory();
+        }
+    }
 
-        @Override
-        public void keyTyped(KeyEvent e) {
-            if (e.getKeyCode() == KeyEvent.VK_TAB) {
-                handler.actionPerformed(new ActionEvent(hcb, 0, "focusDown"));
+    /**
+     * Updates the table editor model with changes in the comboboxes.
+     *
+     * The lock prevents loops in change notifications, eg. the combobox
+     * notifies the table model and the table model notifies the combobox, which
+     * throws IllegalStateException.
+     *
+     * @param text The {@code JTextField} of the combobox editor.
+     * @since xxx
+     */
+    private void updateModel(JTextField text) {
+        if (!locked) {
+            locked = true;
+            try {
+                String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source"
+                String value = text.getText();
+                model.put(tag, value.isEmpty() ? null : value); // remove tags with empty values
+            } finally {
+                locked = false;
             }
         }
     }
 
     /**
-     * Updates the changeset comment model upon changes in the input field.
+     * Save all outstanding edits to the model.
+     * @see UploadDialog#saveEdits
+     * @since xxx
      */
-    static class CommentModelListener extends FocusAdapter implements ActionListener, DocumentListener {
+    public void saveEdits() {
+        updateModel(hcbUploadComment.getEditorComponent());
+        hcbUploadComment.addCurrentItemToHistory();
+        updateModel(hcbUploadSource.getEditorComponent());
+        hcbUploadSource.addCurrentItemToHistory();
+    }
 
-        private final HistoryComboBox source;
-        private final ChangesetCommentModel destination;
-
-        CommentModelListener(HistoryComboBox source, ChangesetCommentModel destination) {
-            this.source = source;
-            this.destination = destination;
+    /**
+     * Returns the UplodDialog that is our ancestor
+     *
+     * @return the UploadDialog or null
+     */
+    private UploadDialog getDialog() {
+        Component d = getRootPane();
+        while ((d = d.getParent()) != null) {
+            if (d instanceof UploadDialog)
+                return (UploadDialog) d;
         }
+        return null;
+    }
 
-        private void setComment() {
-            SwingUtilities.invokeLater(() -> destination.setComment(source.getText()));
-        }
+    /**
+     * Update the model when the selection changes in a combobox.
+     * @param e The action event.
+     */
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        getDialog().setFocusToUploadButton();
+    }
 
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            setComment();
-        }
+    @Override
+    public void focusGained(FocusEvent e) {
+    }
 
-        @Override
-        public void focusLost(FocusEvent e) {
-            setComment();
+    /**
+     * Update the model and combobox history when a combobox editor loses focus.
+     */
+    @Override
+    public void focusLost(FocusEvent e) {
+        Object c = e.getSource();
+        if (c instanceof JTextField) {
+            updateModel((JTextField) c);
+            updateHistory((JTextField) c);
         }
+    }
 
-        @Override
-        public void insertUpdate(DocumentEvent e) {
-            setComment();
+    /**
+     * Updates the table editor model upon changes in the "review" checkbox.
+     */
+    @Override
+    public void itemStateChanged(ItemEvent e) {
+        if (!locked) {
+            locked = true;
+            try {
+                model.put("review_requested", e.getStateChange() == ItemEvent.SELECTED ? "yes" : null);
+            } finally {
+                locked = false;
+            }
         }
-
-        @Override
-        public void removeUpdate(DocumentEvent e) {
-            setComment();
-        }
-
-        @Override
-        public void changedUpdate(DocumentEvent e) {
-            setComment();
-        }
     }
 
     /**
-     * Observes the changeset comment model and keeps the comment input field
-     * in sync with the current changeset comment
+     * Updates the controls upon changes in the table editor model.
      */
-    static class ChangesetCommentChangeListener implements ChangeListener {
-
-        private final HistoryComboBox destination;
-
-        ChangesetCommentChangeListener(HistoryComboBox destination) {
-            this.destination = destination;
-        }
-
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            if (!(e.getSource() instanceof ChangesetCommentModel)) return;
-            String newComment = ((ChangesetCommentModel) e.getSource()).getComment();
-            if (!destination.getText().trim().equals(newComment)) {
-                destination.setText(newComment);
+    @Override
+    public void tableChanged(TableModelEvent e) {
+        if (!locked) {
+            locked = true;
+            try {
+                hcbUploadComment.setText(get("comment"));
+                hcbUploadSource.setText(get("source"));
+                cbRequestReview.setSelected(get("review_requested").equals("yes"));
+            } finally {
+                locked = false;
             }
         }
     }
 
     /**
-     * Observes the changeset review model and keeps the review checkbox
-     * in sync with the current changeset review request
+     * Set the focus directly to the upload button if "Enter" key is pressed in any combobox.
      */
-    class ChangesetReviewChangeListener implements ChangeListener {
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            if (!(e.getSource() instanceof ChangesetReviewModel)) return;
-            boolean newState = ((ChangesetReviewModel) e.getSource()).isReviewRequested();
-            if (cbRequestReview.isSelected() != newState) {
-                cbRequestReview.setSelected(newState);
-            }
+    @Override
+    public void keyTyped(KeyEvent e) {
+        if (e.getKeyChar() == KeyEvent.VK_ENTER) {
+            getDialog().setFocusToUploadButton();
         }
     }
+
+    @Override
+    public void keyPressed(KeyEvent e) {
+    }
+
+    @Override
+    public void keyReleased(KeyEvent e) {
+    }
 }
Index: src/org/openstreetmap/josm/gui/io/ChangesetCommentModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/ChangesetCommentModel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/ChangesetCommentModel.java	(nonexistent)
@@ -1,53 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.io;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import org.openstreetmap.josm.gui.util.ChangeNotifier;
-import org.openstreetmap.josm.tools.Utils;
-
-/**
- * ChangesetCommentModel is an observable model for the changeset comment edited
- * in the {@link UploadDialog}.
- * @since 3133
- */
-public class ChangesetCommentModel extends ChangeNotifier {
-    private String comment = "";
-
-    /**
-     * Sets the current changeset comment and notifies observers if the comment has changed.
-     *
-     * @param comment the new upload comment. Empty string assumed if null.
-     */
-    public void setComment(String comment) {
-        String oldValue = this.comment;
-        this.comment = comment == null ? "" : comment.trim();
-        if (!Objects.equals(oldValue, this.comment)) {
-            fireStateChanged();
-        }
-    }
-
-    /**
-     * Replies the current changeset comment in this model.
-     *
-     * @return the current changeset comment in this model.
-     */
-    public String getComment() {
-        return comment == null ? "" : comment;
-    }
-
-    /**
-     * Extracts the list of hashtags from the comment text.
-     * @return the list of hashtags from the comment text. Can be empty, but not null.
-     * @since 13109
-     */
-    public List<String> findHashTags() {
-        return Arrays.stream(comment.split("\\s", -1))
-                .map(s -> Utils.strip(s, ",;"))
-                .filter(s -> s.matches("#[a-zA-Z][a-zA-Z_\\-0-9]+"))
-                .collect(Collectors.toList());
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/io/ChangesetCommentModel.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/io/ChangesetManagementPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/ChangesetManagementPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/ChangesetManagementPanel.java	(working copy)
@@ -28,7 +28,6 @@
 import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
 import org.openstreetmap.josm.spi.preferences.Config;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
@@ -55,14 +54,18 @@
     private JCheckBox cbCloseAfterUpload;
     private OpenChangesetComboBoxModel model;
 
+    /** the changeset comment model */
+    private final transient UploadDialogModel uploadDialogModel;
+
     /**
      * Constructs a new {@code ChangesetManagementPanel}.
      *
-     * @param changesetCommentModel the changeset comment model. Must not be null.
-     * @throws IllegalArgumentException if {@code changesetCommentModel} is null
+     * @param uploadDialogModel The tag editor model.
+     *
+     * @since xxx (signature)
      */
-    public ChangesetManagementPanel(ChangesetCommentModel changesetCommentModel) {
-        CheckParameterUtil.ensureParameterNotNull(changesetCommentModel, "changesetCommentModel");
+    public ChangesetManagementPanel(UploadDialogModel uploadDialogModel) {
+        this.uploadDialogModel = uploadDialogModel;
         build();
         refreshGUI();
     }
@@ -272,6 +275,7 @@
                 }
                 Changeset cs = (Changeset) cbOpenChangesets.getSelectedItem();
                 if (cs == null) return;
+                uploadDialogModel.putAll(getSelectedChangeset().getKeys());
                 firePropertyChange(SELECTED_CHANGESET_PROP, null, cs);
             }
         }
@@ -279,7 +283,6 @@
 
     /**
      * Refreshes the list of open changesets
-     *
      */
     class RefreshAction extends AbstractAction {
         RefreshAction() {
@@ -295,7 +298,6 @@
 
     /**
      * Closes the currently selected changeset
-     *
      */
     class CloseChangesetAction extends AbstractAction implements ItemListener {
         CloseChangesetAction() {
Index: src/org/openstreetmap/josm/gui/io/ChangesetReviewModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/ChangesetReviewModel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/ChangesetReviewModel.java	(nonexistent)
@@ -1,35 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.io;
-
-import org.openstreetmap.josm.gui.util.ChangeNotifier;
-
-/**
- * ChangesetReviewModel is an observable model for the changeset review requested
- * in the {@link UploadDialog}.
- * @since 12719
- */
-public class ChangesetReviewModel extends ChangeNotifier {
-    private boolean review;
-
-    /**
-     * Sets the current changeset review request state and notifies observers if it has changed.
-     *
-     * @param review the new review request state
-     */
-    public void setReviewRequested(boolean review) {
-        boolean oldValue = this.review;
-        this.review = review;
-        if (oldValue != this.review) {
-            fireStateChanged();
-        }
-    }
-
-    /**
-     * Determines if a changeset review has been requested.
-     *
-     * @return {@code true} if a changeset review has been requested
-     */
-    public boolean isReviewRequested() {
-        return review;
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/io/ChangesetReviewModel.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/io/IUploadDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/IUploadDialog.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/IUploadDialog.java	(working copy)
@@ -65,10 +65,4 @@
      * Handles illegal chunk size.
      */
     void handleIllegalChunkSize();
-
-    /**
-     * Forces update of comment/source model if matching text field is active.
-     * @since 14977
-     */
-    void forceUpdateActiveField();
 }
Index: src/org/openstreetmap/josm/gui/io/TagSettingsPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/TagSettingsPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/TagSettingsPanel.java	(nonexistent)
@@ -1,185 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.io;
-
-import java.awt.BorderLayout;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-import javax.swing.JPanel;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.TableModelEvent;
-import javax.swing.event.TableModelListener;
-
-import org.openstreetmap.josm.data.osm.Changeset;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
-import org.openstreetmap.josm.gui.tagging.TagModel;
-import org.openstreetmap.josm.spi.preferences.Config;
-
-/**
- * Tag settings panel of upload dialog.
- * @since 2599
- */
-public class TagSettingsPanel extends JPanel implements TableModelListener {
-
-    /** checkbox for selecting whether an atomic upload is to be used  */
-    private final TagEditorPanel pnlTagEditor = new TagEditorPanel(null, null, Changeset.MAX_CHANGESET_TAG_LENGTH);
-    /** the model for the changeset comment */
-    private final transient ChangesetCommentModel changesetCommentModel;
-    private final transient ChangesetCommentModel changesetSourceModel;
-    private final transient ChangesetReviewModel changesetReviewModel;
-
-    /**
-     * Creates a new panel
-     *
-     * @param changesetCommentModel the changeset comment model. Must not be null.
-     * @param changesetSourceModel the changeset source model. Must not be null.
-     * @param changesetReviewModel the model for the changeset review. Must not be null.
-     * @throws NullPointerException if a model is null
-     * @since 12719 (signature)
-     */
-    public TagSettingsPanel(ChangesetCommentModel changesetCommentModel, ChangesetCommentModel changesetSourceModel,
-            ChangesetReviewModel changesetReviewModel) {
-        this.changesetCommentModel = Objects.requireNonNull(changesetCommentModel, "changesetCommentModel");
-        this.changesetSourceModel = Objects.requireNonNull(changesetSourceModel, "changesetSourceModel");
-        this.changesetReviewModel = Objects.requireNonNull(changesetReviewModel, "changesetReviewModel");
-        changesetCommentModel.addChangeListener(new ChangesetCommentChangeListener("comment", "hashtags"));
-        changesetSourceModel.addChangeListener(new ChangesetCommentChangeListener("source"));
-        changesetReviewModel.addChangeListener(new ChangesetReviewChangeListener());
-        build();
-        pnlTagEditor.getModel().addTableModelListener(this);
-    }
-
-    protected void build() {
-        setLayout(new BorderLayout());
-        add(pnlTagEditor, BorderLayout.CENTER);
-    }
-
-    protected void setProperty(String key, String value) {
-        String val = (value == null ? "" : value).trim();
-        String commentInTag = getTagEditorValue(key);
-        if (val.equals(commentInTag))
-            return;
-
-        if (val.isEmpty()) {
-            pnlTagEditor.getModel().delete(key);
-            return;
-        }
-        TagModel tag = pnlTagEditor.getModel().get(key);
-        if (tag == null) {
-            tag = new TagModel(key, val);
-            pnlTagEditor.getModel().add(tag);
-        } else {
-            pnlTagEditor.getModel().updateTagValue(tag, val);
-        }
-    }
-
-    protected String getTagEditorValue(String key) {
-        TagModel tag = pnlTagEditor.getModel().get(key);
-        return tag == null ? null : tag.getValue();
-    }
-
-    /**
-     * Initialize panel from the given tags.
-     * @param tags the tags used to initialize the panel
-     */
-    public void initFromTags(Map<String, String> tags) {
-        pnlTagEditor.getModel().initFromTags(tags);
-    }
-
-    /**
-     * Replies the map with the current tags in the tag editor model.
-     * @param keepEmpty {@code true} to keep empty tags
-     * @return the map with the current tags in the tag editor model.
-     */
-    public Map<String, String> getTags(boolean keepEmpty) {
-        forceCommentFieldReload();
-        return pnlTagEditor.getModel().getTags(keepEmpty);
-    }
-
-    /**
-     * Initializes the panel for user input
-     */
-    public void startUserInput() {
-        pnlTagEditor.initAutoCompletion(MainApplication.getLayerManager().getEditLayer());
-    }
-
-    /* -------------------------------------------------------------------------- */
-    /* Interface TableChangeListener                                              */
-    /* -------------------------------------------------------------------------- */
-    @Override
-    public void tableChanged(TableModelEvent e) {
-        changesetCommentModel.setComment(getTagEditorValue("comment"));
-        changesetSourceModel.setComment(getTagEditorValue("source"));
-        changesetReviewModel.setReviewRequested("yes".equals(getTagEditorValue("review_requested")));
-    }
-
-    /**
-     * Force update the fields if the user is currently changing them. See #5676
-     */
-    private void forceCommentFieldReload() {
-        setProperty("comment", changesetCommentModel.getComment());
-        setProperty("source", changesetSourceModel.getComment());
-        setProperty("review_requested", changesetReviewModel.isReviewRequested() ? "yes" : null);
-    }
-
-    /**
-     * Observes the changeset comment model and keeps the tag editor in sync
-     * with the current changeset comment
-     */
-    class ChangesetCommentChangeListener implements ChangeListener {
-
-        private final String key;
-        private final String hashtagsKey;
-
-        ChangesetCommentChangeListener(String key) {
-            this(key, null);
-        }
-
-        ChangesetCommentChangeListener(String key, String hashtagsKey) {
-            this.key = key;
-            this.hashtagsKey = hashtagsKey;
-        }
-
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            if (e.getSource() instanceof ChangesetCommentModel) {
-                ChangesetCommentModel model = ((ChangesetCommentModel) e.getSource());
-                String newValue = model.getComment();
-                String oldValue = Optional.ofNullable(getTagEditorValue(key)).orElse("");
-                if (!oldValue.equals(newValue)) {
-                    setProperty(key, newValue);
-                    if (hashtagsKey != null && Config.getPref().getBoolean("upload.changeset.hashtags", true)) {
-                        String newHashTags = String.join(";", model.findHashTags());
-                        String oldHashTags = Optional.ofNullable(getTagEditorValue(hashtagsKey)).orElse("");
-                        if (!oldHashTags.equals(newHashTags)) {
-                            setProperty(hashtagsKey, newHashTags);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Observes the changeset review model and keeps the tag editor in sync
-     * with the current changeset review request
-     */
-    class ChangesetReviewChangeListener implements ChangeListener {
-
-        private static final String KEY = "review_requested";
-
-        @Override
-        public void stateChanged(ChangeEvent e) {
-            if (e.getSource() instanceof ChangesetReviewModel) {
-                boolean newState = ((ChangesetReviewModel) e.getSource()).isReviewRequested();
-                boolean oldState = "yes".equals(Optional.ofNullable(getTagEditorValue(KEY)).orElse(""));
-                if (oldState != newState) {
-                    setProperty(KEY, newState ? "yes" : null);
-                }
-            }
-        }
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/io/TagSettingsPanel.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/io/UploadDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadDialog.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/io/UploadDialog.java	(working copy)
@@ -17,16 +17,13 @@
 import java.beans.PropertyChangeListener;
 import java.lang.Character.UnicodeBlock;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.swing.AbstractAction;
@@ -39,7 +36,6 @@
 import javax.swing.border.TitledBorder;
 
 import org.openstreetmap.josm.data.APIDataSet;
-import org.openstreetmap.josm.data.Version;
 import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -47,6 +43,7 @@
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
 import org.openstreetmap.josm.gui.help.HelpUtil;
+import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.MultiLineFlowLayout;
 import org.openstreetmap.josm.gui.util.WindowGeometry;
@@ -60,6 +57,7 @@
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.InputMapUtils;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -71,9 +69,6 @@
     /** the unique instance of the upload dialog */
     private static UploadDialog uploadDialog;
 
-    /** the "created_by" changeset OSM key */
-    private static final String CREATED_BY = "created_by";
-
     /** the panel with the objects to upload */
     private UploadedObjectsSummaryPanel pnlUploadedObjects;
     /** the panel to select the changeset used */
@@ -84,17 +79,17 @@
     private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel;
 
     private TitledBorder tagSettingsBorder;
-    /** checkbox for selecting whether an atomic upload is to be used  */
-    private TagSettingsPanel pnlTagSettings;
+    /** a border around the tag editor panel */
+    private JPanel pnlTagEditorBorder;
+    /** the tag editor panel */
+    private TagEditorPanel pnlTagEditor;
     /** the tabbed pane used below of the list of primitives  */
     private JTabbedPane tpConfigPanels;
     /** the upload button */
     private JButton btnUpload;
 
-    /** the changeset comment model keeping the state of the changeset comment */
-    private final transient ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel();
-    private final transient ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel();
-    private final transient ChangesetReviewModel changesetReviewModel = new ChangesetReviewModel();
+    /** the model keeping the state of the changeset tags */
+    private final transient UploadDialogModel model = new UploadDialogModel();
 
     private transient DataSet dataSet;
 
@@ -137,22 +132,27 @@
         tpConfigPanels = new CompactTabbedPane();
         splitPane.setRightComponent(tpConfigPanels);
 
-        pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
+        pnlBasicUploadSettings = new BasicUploadSettingsPanel(model);
         tpConfigPanels.add(pnlBasicUploadSettings);
         tpConfigPanels.setTitleAt(0, tr("Description"));
-        tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use"));
+        tpConfigPanels.setToolTipTextAt(0, tr("Describe the changes you made"));
 
+        JPanel pnlSettings = new JPanel(new GridBagLayout());
+        pnlTagEditorBorder = new JPanel(new BorderLayout());
         tagSettingsBorder = BorderFactory.createTitledBorder(tr("Tags of new changeset"));
-        pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
-        pnlTagSettings.setBorder(tagSettingsBorder);
-        pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel);
+        pnlTagEditorBorder.setBorder(tagSettingsBorder);
+        pnlTagEditor = new TagEditorPanel(model, null, Changeset.MAX_CHANGESET_TAG_LENGTH);
+        pnlTagEditorBorder.add(pnlTagEditor, BorderLayout.CENTER);
+
+        pnlChangesetManagement = new ChangesetManagementPanel(model);
         pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel();
-        JPanel pnlChangeset = new JPanel(new GridBagLayout());
-        pnlChangeset.add(pnlChangesetManagement, GBC.eop().fill(GBC.HORIZONTAL));
-        pnlChangeset.add(pnlUploadStrategySelectionPanel, GBC.eop().fill(GBC.HORIZONTAL));
-        pnlChangeset.add(pnlTagSettings, GBC.eol().fill(GBC.BOTH));
-        tpConfigPanels.add(pnlChangeset);
+        pnlSettings.add(pnlChangesetManagement, GBC.eop().fill(GBC.HORIZONTAL));
+        pnlSettings.add(pnlUploadStrategySelectionPanel, GBC.eop().fill(GBC.HORIZONTAL));
+        pnlSettings.add(pnlTagEditorBorder, GBC.eol().fill(GBC.BOTH));
+
+        tpConfigPanels.add(pnlSettings);
         tpConfigPanels.setTitleAt(1, tr("Settings"));
+        tpConfigPanels.setToolTipTextAt(1, tr("Decide how to upload the data and which changeset to use"));
 
         JPanel pnl = new JPanel(new BorderLayout());
         pnl.add(splitPane, BorderLayout.CENTER);
@@ -180,6 +180,8 @@
         CancelAction cancelAction = new CancelAction(this);
         pnl.add(new JButton(cancelAction));
         InputMapUtils.addEscapeAction(getRootPane(), cancelAction);
+
+        // -- help button
         pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload"))));
         HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/Upload"));
         return pnl;
@@ -200,11 +202,11 @@
         pnlChangesetManagement.addPropertyChangeListener(
                 pnlBasicUploadSettings.getUploadParameterSummaryPanel()
         );
-        pnlChangesetManagement.addPropertyChangeListener(this);
+        pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
         pnlUploadedObjects.addPropertyChangeListener(
                 pnlBasicUploadSettings.getUploadParameterSummaryPanel()
         );
-        pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
+        pnlUploadStrategySelectionPanel.addPropertyChangeListener(this);
         pnlUploadStrategySelectionPanel.addPropertyChangeListener(
                 pnlBasicUploadSettings.getUploadParameterSummaryPanel()
         );
@@ -217,8 +219,6 @@
                 () -> tpConfigPanels.setSelectedIndex(2)
         );
 
-        pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers(e -> btnUpload.requestFocusInWindow());
-
         // Enable/disable the upload button if at least an upload validator rejects upload
         pnlBasicUploadSettings.getUploadTextValidators().forEach(v -> v.addChangeListener(e -> btnUpload.setEnabled(
                 pnlBasicUploadSettings.getUploadTextValidators().stream().noneMatch(UploadTextComponentValidator::isUploadRejected))));
@@ -229,6 +229,24 @@
     }
 
     /**
+     * Initializes this life cycle of the dialog.
+     *
+     * Initializes the dialog each time before it is made visible. We cannot do
+     * this in the constructor because the dialog is a singleton.
+     *
+     * @param dataSet The Dataset we want to upload
+     * @since xxx
+     */
+    public void initLifeCycle(DataSet dataSet) {
+        Map<String, String> map = new HashMap<>();
+        this.dataSet = dataSet;
+        pnlBasicUploadSettings.initLifeCycle(map);
+        model.clear();
+        model.putAll(map);
+        model.putAll(this.dataSet);
+    }
+
+    /**
      * Sets the collection of primitives to upload
      *
      * @param toUpload the dataset with the objects to upload. If null, assumes the empty
@@ -252,101 +270,13 @@
     }
 
     /**
-     * Sets the tags for this upload based on (later items overwrite earlier ones):
-     * <ul>
-     * <li>previous "source" and "comment" input</li>
-     * <li>the tags set in the dataset (see {@link DataSet#getChangeSetTags()})</li>
-     * <li>the tags from the selected open changeset</li>
-     * <li>the JOSM user agent (see {@link Version#getAgentString(boolean)})</li>
-     * </ul>
-     *
-     * @param dataSet to obtain the tags set in the dataset
+     * Sets the input focus to upload button.
+     * @since xxx
      */
-    public void setChangesetTags(DataSet dataSet) {
-        setChangesetTags(dataSet, false);
+    public void setFocusToUploadButton() {
+        btnUpload.requestFocus();
     }
 
-    /**
-     * Sets the tags for this upload based on (later items overwrite earlier ones):
-     * <ul>
-     * <li>previous "source" and "comment" input</li>
-     * <li>the tags set in the dataset (see {@link DataSet#getChangeSetTags()})</li>
-     * <li>the tags from the selected open changeset</li>
-     * <li>the JOSM user agent (see {@link Version#getAgentString(boolean)})</li>
-     * </ul>
-     *
-     * @param dataSet to obtain the tags set in the dataset
-     * @param keepSourceComment if {@code true}, keep upload {@code source} and {@code comment} current values from models
-     */
-    private void setChangesetTags(DataSet dataSet, boolean keepSourceComment) {
-        final Map<String, String> tags = new HashMap<>();
-
-        // obtain from previous input
-        if (!keepSourceComment) {
-            tags.put("source", getLastChangesetSourceFromHistory());
-            tags.put("comment", getCommentWithDataSetHashTag(getLastChangesetCommentFromHistory(), dataSet));
-        }
-
-        // obtain from dataset
-        if (dataSet != null) {
-            tags.putAll(dataSet.getChangeSetTags());
-        }
-        this.dataSet = dataSet;
-
-        // obtain from selected open changeset
-        if (pnlChangesetManagement.getSelectedChangeset() != null) {
-            tags.putAll(pnlChangesetManagement.getSelectedChangeset().getKeys());
-        }
-
-        // set/adapt created_by
-        final String agent = Version.getInstance().getAgentString(false);
-        final String createdBy = tags.get(CREATED_BY);
-        if (createdBy == null || createdBy.isEmpty()) {
-            tags.put(CREATED_BY, agent);
-        } else if (!createdBy.contains(agent)) {
-            tags.put(CREATED_BY, createdBy + ';' + agent);
-        }
-
-        // remove empty values
-        tags.keySet().removeIf(key -> {
-            final String v = tags.get(key);
-            return v == null || v.isEmpty();
-        });
-
-        // ignore source/comment to keep current values from models ?
-        if (keepSourceComment) {
-            tags.put("source", changesetSourceModel.getComment());
-            tags.put("comment", getCommentWithDataSetHashTag(changesetCommentModel.getComment(), dataSet));
-        }
-
-        pnlTagSettings.initFromTags(tags);
-        pnlTagSettings.tableChanged(null);
-        pnlBasicUploadSettings.discardAllUndoableEdits();
-    }
-
-    /**
-     * Returns the given comment with appended hashtags from dataset changeset tags, if not already present.
-     * @param comment changeset comment. Can be null
-     * @param dataSet optional dataset, which can contain hashtags in its changeset tags
-     * @return comment with dataset changesets tags, if any, not duplicated
-     */
-    static String getCommentWithDataSetHashTag(String comment, DataSet dataSet) {
-        StringBuilder result = comment == null ? new StringBuilder() : new StringBuilder(comment);
-        if (dataSet != null) {
-            String hashtags = dataSet.getChangeSetTags().get("hashtags");
-            if (hashtags != null) {
-                Set<String> sanitizedHashtags = new LinkedHashSet<>();
-                for (String hashtag : hashtags.split(";", -1)) {
-                    sanitizedHashtags.add(hashtag.startsWith("#") ? hashtag : "#" + hashtag);
-                }
-                if (!sanitizedHashtags.isEmpty()) {
-                    result.append(' ').append(String.join(" ", sanitizedHashtags));
-                }
-            }
-        }
-        return result.toString();
-    }
-
     @Override
     public void rememberUserInput() {
         pnlBasicUploadSettings.rememberUserInput();
@@ -359,7 +289,7 @@
     public void startUserInput() {
         tpConfigPanels.setSelectedIndex(0);
         pnlBasicUploadSettings.startUserInput();
-        pnlTagSettings.startUserInput();
+        pnlTagEditor.initAutoCompletion(MainApplication.getLayerManager().getEditLayer());
         pnlUploadStrategySelectionPanel.initFromPreferences();
         UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
         pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
@@ -374,7 +304,7 @@
      */
     public Changeset getChangeset() {
         Changeset cs = Optional.ofNullable(pnlChangesetManagement.getSelectedChangeset()).orElseGet(Changeset::new);
-        cs.setKeys(pnlTagSettings.getTags(false));
+        cs.setKeys(model.getTags(false));
         return cs;
     }
 
@@ -394,14 +324,24 @@
         return spec;
     }
 
+    /**
+     * Get the upload dialog model.
+     *
+     * @return The model.
+     * @since xxx
+     */
+    public UploadDialogModel getModel() {
+        return model;
+    }
+
     @Override
     public String getUploadComment() {
-        return changesetCommentModel.getComment();
+        return model.getValue("comment");
     }
 
     @Override
     public String getUploadSource() {
-        return changesetSourceModel.getComment();
+        return model.getValue("source");
     }
 
     @Override
@@ -495,13 +435,13 @@
 
         @Override
         public void actionPerformed(ActionEvent e) {
-            // force update of model in case dialog is closed before focus lost event, see #17452
-            dialog.forceUpdateActiveField();
+            Map<String, String> tags = dialog.getTags(true);
+            Logging.info("Starting upload with tags {0}", tags);
 
             /* test for empty tags in the changeset metadata and proceed only after user's confirmation.
              * though, accept if key and value are empty (cf. xor). */
             List<String> emptyChangesetTags = new ArrayList<>();
-            for (final Entry<String, String> i : dialog.getTags(true).entrySet()) {
+            for (final Entry<String, String> i : tags.entrySet()) {
                 final boolean isKeyEmpty = Utils.isStripEmpty(i.getKey());
                 final boolean isValueEmpty = Utils.isStripEmpty(i.getValue());
                 final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey());
@@ -588,7 +528,6 @@
     public void propertyChange(PropertyChangeEvent evt) {
         if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) {
             Changeset cs = (Changeset) evt.getNewValue();
-            setChangesetTags(dataSet, cs == null); // keep comment/source of first tab for new changesets
             if (cs == null) {
                 tagSettingsBorder.setTitle(tr("Tags of new changeset"));
             } else {
@@ -609,10 +548,6 @@
                 case "osm-server.url":
                     osmServerUrlChanged(e.getNewValue());
                     break;
-                case BasicUploadSettingsPanel.HISTORY_KEY:
-                case BasicUploadSettingsPanel.SOURCE_HISTORY_KEY:
-                    pnlBasicUploadSettings.refreshHistoryComboBoxes();
-                    break;
                 default:
                     return;
             }
@@ -629,34 +564,13 @@
         setTitle(tr("Upload to ''{0}''", url));
     }
 
-    private static String getLastChangesetTagFromHistory(String historyKey, List<String> def) {
-        Collection<String> history = Config.getPref().getList(historyKey, def);
-        long age = System.currentTimeMillis() / 1000 - BasicUploadSettingsPanel.getHistoryLastUsedKey();
-        if (age < BasicUploadSettingsPanel.getHistoryMaxAgeKey() && !history.isEmpty()) {
-            return history.iterator().next();
-        }
-        return null;
-    }
-
-    /**
-     * Returns the last changeset comment from history.
-     * @return the last changeset comment from history
-     */
-    public static String getLastChangesetCommentFromHistory() {
-        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY, new ArrayList<String>());
-    }
-
-    /**
-     * Returns the last changeset source from history.
-     * @return the last changeset source from history
-     */
-    public static String getLastChangesetSourceFromHistory() {
-        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources());
-    }
-
+    /* -------------------------------------------------------------------------- */
+    /* Interface IUploadDialog                                                    */
+    /* -------------------------------------------------------------------------- */
     @Override
     public Map<String, String> getTags(boolean keepEmpty) {
-        return pnlTagSettings.getTags(keepEmpty);
+        saveEdits();
+        return model.getTags(keepEmpty);
     }
 
     @Override
@@ -676,11 +590,18 @@
         tpConfigPanels.setSelectedIndex(0);
     }
 
-    @Override
-    public void forceUpdateActiveField() {
-        if (tpConfigPanels.getSelectedComponent() == pnlBasicUploadSettings) {
-            pnlBasicUploadSettings.forceUpdateActiveField();
-        }
+    /**
+     * Save all outstanding edits to the model.
+     * <p>
+     * The combobox editors and the tag cell editor need to be manually saved
+     * because they normally save on focus loss, eg. when the "Upload" button is
+     * pressed, but there's no focus change when Ctrl+Enter is pressed.
+     *
+     * @since xxx
+     */
+    public void saveEdits() {
+        pnlBasicUploadSettings.saveEdits();
+        pnlTagEditor.saveEdits();
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/io/UploadDialogModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/io/UploadDialogModel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/io/UploadDialogModel.java	(working copy)
@@ -0,0 +1,168 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.tagging.TagEditorModel;
+import org.openstreetmap.josm.gui.tagging.TagModel;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * A model for the upload dialog
+ *
+ * @since xxx
+ */
+public class UploadDialogModel extends TagEditorModel {
+    /** the "created_by" changeset OSM key */
+    private static final String CREATED_BY = "created_by";
+    /** the user-agent */
+    private final String agent = Version.getInstance().getAgentString(false);
+    /** whether to extract hashtags from comment */
+    private final boolean hashtags = Config.getPref().getBoolean("upload.changeset.hashtags", true);
+
+    /** a lock to prevent loops  */
+    private boolean locked;
+
+    @Override
+    public void fireTableDataChanged() {
+        if (!locked) {
+            try {
+                locked = true;
+                // add "hashtags" if any
+                if (hashtags) {
+                    put("hashtags", findHashTags(getValue("comment")));
+                }
+                // add/update "created_by"
+                final String createdBy = getValue(CREATED_BY);
+                if (createdBy.isEmpty()) {
+                    put(CREATED_BY, agent);
+                } else if (!createdBy.contains(agent)) {
+                    put(CREATED_BY, createdBy + ';' + agent);
+                }
+                super.fireTableDataChanged();
+            } finally {
+                locked = false;
+            }
+        }
+    }
+
+    /**
+     * Get the value of a key.
+     *
+     * @param key The key to retrieve
+     * @return The value (may be null)
+     */
+    public String getValue(String key) {
+        TagModel tag = get(key);
+        return tag == null ? "" : tag.getValue();
+    }
+
+    /**
+     * Extracts the list of hashtags from the comment text.
+     * @param comment The comment with the hashtags
+     * @return the hashtags separated by ";" or null
+     */
+    String findHashTags(String comment) {
+        String hashtags = String.join(";",
+            Arrays.stream(comment.split("\\s", -1))
+                .map(s -> Utils.strip(s, ",;"))
+                .filter(s -> s.matches("#[a-zA-Z][-_a-zA-Z0-9]+"))
+                .collect(Collectors.toList()));
+        return hashtags.isEmpty() ? null : hashtags;
+    }
+
+    /**
+     * Returns the given comment with appended hashtags from dataset changeset tags, if not already present.
+     * @param comment changeset comment. Can be null
+     * @param dataSet optional dataset, which can contain hashtags in its changeset tags
+     * @return comment with dataset changesets tags, if any, not duplicated
+     */
+    static String addHashTagsFromDataSet(String comment, DataSet dataSet) {
+        StringBuilder result = comment == null ? new StringBuilder() : new StringBuilder(comment);
+        if (dataSet != null) {
+            String hashtags = dataSet.getChangeSetTags().get("hashtags");
+            if (hashtags != null) {
+                Set<String> sanitizedHashtags = new LinkedHashSet<>();
+                for (String hashtag : hashtags.split(";", -1)) {
+                    sanitizedHashtags.add(hashtag.startsWith("#") ? hashtag : "#" + hashtag);
+                }
+                if (!sanitizedHashtags.isEmpty()) {
+                    result.append(' ').append(String.join(" ", sanitizedHashtags));
+                }
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Inserts/updates/deletes a tag.
+     *
+     * Existing keys are updated. Others are added. A value of {@code null}
+     * deletes the key.
+     *
+     * @param key The key of the tag to insert.
+     * @param value The value of the tag to insert.
+     */
+    private void doPut(String key, String value) {
+        List<TagModel> l = tags.stream().filter(tm -> tm.getName().equals(key)).collect(Collectors.toList());
+        if (!l.isEmpty()) {
+            if (value != null)
+                l.get(0).setValue(value);
+            else
+                tags.remove(l.get(0));
+        } else if (value != null) {
+            tags.add(new TagModel(key, value));
+        }
+    }
+
+    /**
+     * Inserts/updates/deletes a tag.
+     *
+     * Existing keys are updated. Others are added. A value of {@code null}
+     * deletes the key.
+     *
+     * @param key The key of the tag to insert.
+     * @param value The value of the tag to insert.
+     */
+    public void put(String key, String value) {
+        commitPendingEdit();
+        doPut(key, value);
+        setDirty(true);
+        fireTableDataChanged();
+    }
+
+    /**
+     * Inserts/updates/deletes all tags from {@code map}.
+     *
+     * Existing keys are updated. Others are added. A value of {@code null}
+     * deletes the key.
+     *
+     * @param map a map of tags to insert or update
+     */
+    public void putAll(Map<String, String> map) {
+        commitPendingEdit();
+        map.forEach((key, value) -> doPut(key, value));
+        setDirty(true);
+        fireTableDataChanged();
+    }
+
+    /**
+     * Inserts all tags from a {@code DataSet}.
+     *
+     * @param dataSet The DataSet to take tags from.
+     */
+    public void putAll(DataSet dataSet) {
+        if (dataSet != null) {
+            putAll(dataSet.getChangeSetTags());
+            put("comment", addHashTagsFromDataSet(getValue("comment"), dataSet));
+        }
+    }
+}

Property changes on: src/org/openstreetmap/josm/gui/io/UploadDialogModel.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java	(working copy)
@@ -22,13 +22,11 @@
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.data.projection.ProjectionConfigurationException;
 import org.openstreetmap.josm.data.projection.Projections;
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
 import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
-import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
@@ -63,7 +61,6 @@
         private void build(String initialText, final ActionListener listener) {
             input = new JosmTextField(30);
             cbInput = new HistoryComboBox();
-            cbInput.setPrototypeDisplayValue(new AutoCompletionItem("xxxx"));
             cbInput.setEditor(new BasicComboBoxEditor() {
                 @Override
                 protected JosmTextField createEditorComponent() {
@@ -73,7 +70,7 @@
             List<String> samples = Arrays.asList(
                     "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90",
                     "+proj=tmerc +lat_0=0 +lon_0=9 +k_0=1 +x_0=3500000 +y_0=0 +ellps=bessel +nadgrids=BETA2007.gsb");
-            cbInput.setPossibleItemsTopDown(Config.getPref().getList("projection.custom.value.history", samples));
+            cbInput.getModel().prefs().load("projection.custom.value.history", samples);
             cbInput.setText(initialText == null ? "" : initialText);
 
             final HtmlPanel errorsPanel = new HtmlPanel();
@@ -145,7 +142,7 @@
 
         public void rememberHistory() {
             cbInput.addCurrentItemToHistory();
-            Config.getPref().putList("projection.custom.value.history", cbInput.getHistory());
+            cbInput.getModel().prefs().save("projection.custom.value.history");
         }
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/server/OsmApiUrlInputPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/server/OsmApiUrlInputPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/preferences/server/OsmApiUrlInputPanel.java	(working copy)
@@ -108,7 +108,7 @@
      */
     public void initFromPreferences() {
         String url = OsmApi.getOsmApi().getServerUrl();
-        tfOsmServerUrl.setPossibleItems(SERVER_URL_HISTORY.get());
+        tfOsmServerUrl.getModel().prefs().load(SERVER_URL_HISTORY);
         if (Config.getUrls().getDefaultOsmApiUrl().equals(url.trim())) {
             cbUseDefaultServerUrl.setSelected(true);
             propagator.propagate(Config.getUrls().getDefaultOsmApiUrl());
@@ -130,7 +130,7 @@
         } else {
             Config.getPref().put("osm-server.url", hmiUrl);
             tfOsmServerUrl.addCurrentItemToHistory();
-            SERVER_URL_HISTORY.put(tfOsmServerUrl.getHistory());
+            tfOsmServerUrl.getModel().prefs().save(SERVER_URL_HISTORY);
         }
         String newUrl = OsmApi.getOsmApi().getServerUrl();
 
Index: src/org/openstreetmap/josm/gui/preferences/server/OverpassServerPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/server/OverpassServerPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/preferences/server/OverpassServerPanel.java	(working copy)
@@ -41,7 +41,7 @@
      * Initializes the panel from preferences
      */
     public final void initFromPreferences() {
-        overpassServer.setPossibleItems(OverpassDownloadReader.OVERPASS_SERVER_HISTORY.get());
+        overpassServer.getModel().prefs().load(OverpassDownloadReader.OVERPASS_SERVER_HISTORY);
         overpassServer.setText(OverpassDownloadReader.OVERPASS_SERVER.get());
         forMultiFetch.setSelected(OverpassDownloadReader.FOR_MULTI_FETCH.get());
     }
@@ -51,7 +51,7 @@
      */
     public final void saveToPreferences() {
         OverpassDownloadReader.OVERPASS_SERVER.put(overpassServer.getText());
-        OverpassDownloadReader.OVERPASS_SERVER_HISTORY.put(overpassServer.getHistory());
+        overpassServer.getModel().prefs().save(OverpassDownloadReader.OVERPASS_SERVER_HISTORY);
         OverpassDownloadReader.FOR_MULTI_FETCH.put(forMultiFetch.isSelected());
     }
 }
Index: src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java	(working copy)
@@ -645,7 +645,7 @@
         this.endEditListener = endEditListener;
     }
 
-    private void commitPendingEdit() {
+    protected void commitPendingEdit() {
         if (endEditListener != null) {
             endEditListener.endCellEditing();
         }
Index: src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/tagging/TagEditorPanel.java	(working copy)
@@ -221,4 +221,13 @@
                 model.getTags(), presetHandler);
         validate();
     }
+
+    /**
+     * Save all outstanding edits to the model.
+     * @see org.openstreetmap.josm.gui.io.UploadDialog#saveEdits
+     * @since xxx
+     */
+    public void saveEdits() {
+        tagTable.endCellEditing();
+    }
 }
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java	(working copy)
@@ -0,0 +1,349 @@
+// 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 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;
+
+/**
+ * An auto-completing ComboBox.
+ * <p>
+ * When the user starts typing, this combobox will suggest the
+ * {@link AutoCompComboBoxModel#findBestCandidate best matching item} from its list.  The items can
+ * be of any type while the items' {@code toString} values are shown in the combobox and editor.
+ *
+ * @author guilhem.bonnefille@gmail.com
+ * @author marcello@perathoner.de
+ * @param <E> the type of the combobox entries
+ * @since xxx
+ */
+public class AutoCompComboBox<E> extends JosmComboBox<E> implements KeyListener {
+
+    /** 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 {
+            int newLen = fb.getDocument().getLength() + string.length();
+            if (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))) {
+                super.insertString(fb, offset, string, attr);
+            }
+        }
+
+        @Override
+        public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attr)
+                throws BadLocationException {
+            int newLen = fb.getDocument().getLength() - length + string.length();
+            if (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))) {
+                super.replace(fb, offset, length, string, attr);
+            }
+        }
+    }
+
+    /**
+     * Constructs an {@code AutoCompletingComboBox}.
+     */
+    public AutoCompComboBox() {
+        this(new AutoCompComboBoxModel<E>());
+    }
+
+    /**
+     * Constructs an {@code AutoCompletingComboBox} with a supplied {@link AutoCompComboBoxModel}.
+     *
+     * @param model the model
+     */
+    public AutoCompComboBox(AutoCompComboBoxModel<E> model) {
+        super(model);
+        Objects.requireNonNull(model, "A model cannot be null.");
+        setEditable(true);
+        final JTextComponent editorComponent = getEditorComponent();
+        editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
+        editorComponent.addKeyListener(this);
+        ((AbstractDocument) editorComponent.getDocument()).setDocumentFilter(new MaxLengthDocumentFilter());
+    }
+
+    /**
+     * Returns the {@link AutoCompComboBoxModel} currently used.
+     *
+     * @return the model
+     */
+    @Override
+    public AutoCompComboBoxModel<E> getModel() {
+        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);
+        }
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * Sets the maximum text length.
+     *
+     * @param length the maximum text length in number of characters
+     */
+    public void setMaxTextLength(int length) {
+        maxTextLength = length;
+    }
+
+    /**
+     * Sets the items of the combobox to the given {@code String}s in reversed order (last element
+     * first).
+     *
+     * @param elems The string items to set
+     * @deprecated Has been moved to the model, where it belongs. Use
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel#addAllStrings} instead. Probably you want to use
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel.Preferences#load} and
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel.Preferences#save}.
+     */
+    @Deprecated
+    public void setPossibleItems(Collection<E> elems) {
+        // We have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
+        LinkedList<E> reversed = new LinkedList<>(elems);
+        Collections.reverse(reversed);
+        setPossibleAcItems(reversed);
+    }
+
+    /**
+     * Sets the items of the combobox to the given {@code String}s in top down order.
+     *
+     * @param elems The strings to set.
+     * @since 15011
+     * @deprecated Has been moved to the model, where it belongs. Use
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel#addAllStrings} instead. Probably you want to use
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel.Preferences#load} and
+     *     {@link org.openstreetmap.josm.gui.widgets.HistoryComboBoxModel.Preferences#save}.
+     */
+    @Deprecated
+    public void setPossibleItemsTopDown(Collection<E> elems) {
+        setPossibleAcItems(elems);
+    }
+
+    /**
+     * Sets the items of the combobox to the given {@code AutoCompletionItem}s.
+     *
+     * @param elems AutoCompletionItem items
+     * @since 12859
+     * @deprecated Use {@link AutoCompComboBoxModel#addAllElements} instead.
+     */
+    @Deprecated
+    public void setPossibleAcItems(Collection<E> elems) {
+        Object oldValue = getEditor().getItem();
+        getModel().removeAllElements();
+        getModel().addAllElements(elems);
+        getEditor().setItem(oldValue);
+    }
+
+    /**
+     * 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
+     * @since xxx (signature)
+     */
+    public boolean setAutocompleteEnabled(boolean enabled) {
+        boolean oldEnabled = this.autocompleteEnabled;
+        this.autocompleteEnabled = enabled;
+        return oldEnabled;
+    }
+
+    /**
+     * Fixes the locale for keyboard input to US-English.
+     * <p>
+     * If the locale is fixed, English keyboard layout will be used by default for this combobox.
+     * All other components can still have different keyboard layout selected.
+     *
+     * @param f if {@code true} use fixed locale
+     */
+    public void setFixedLocale(boolean f) {
+        useFixedLocale = f;
+        if (useFixedLocale) {
+            Locale oldLocale = privateInputContext.getLocale();
+            Logging.info("Using English input method");
+            if (!privateInputContext.selectInputMethod(new Locale("en", "US"))) {
+                // Unable to use English keyboard layout, disable the feature
+                Logging.warn("Unable to use English input method");
+                useFixedLocale = false;
+                if (oldLocale != null) {
+                    Logging.info("Restoring input method to " + oldLocale);
+                    if (!privateInputContext.selectInputMethod(oldLocale)) {
+                        Logging.warn("Unable to restore input method to " + oldLocale);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public InputContext getInputContext() {
+        if (useFixedLocale) {
+            return privateInputContext;
+        }
+        return super.getInputContext();
+    }
+
+    /*
+     * The KeyListener 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());
+        }
+    }
+
+    @Override
+    public void keyPressed(KeyEvent e) {
+    }
+
+    @Override
+    public void keyReleased(KeyEvent e) {
+    }
+}

Property changes on: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBox.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java	(working copy)
@@ -0,0 +1,359 @@
+// 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.data.preferences.ListProperty;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * A data model for the {@link AutoCompComboBox}
+ *
+ * @author marcello@perathoner.de
+ * @param <E> The element type.
+ * @since xxx
+ */
+public class AutoCompComboBoxModel<E> extends AbstractListModel<E> implements MutableComboBoxModel<E>, Iterable<E> {
+
+    /**
+     * The comparator used by {@link #findBestCandidate}
+     * <p>
+     * The comparator is used exclusively for autocompleting, and not for sorting the combobox
+     * entries.  The default comparator sorts elements in alphabetical order according to
+     * {@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}.
+     */
+    public AutoCompComboBoxModel() {
+        setComparator(Comparator.comparing(E::toString));
+    }
+
+    /**
+     * Constructs a new empty model with a custom {@link #comparator}.
+     *
+     * @param comparator A custom {@link #comparator}.
+     */
+    public AutoCompComboBoxModel(Comparator<E> comparator) {
+        setComparator(comparator);
+    }
+
+    /**
+     * Sets a custom {@link #comparator}.
+     * <p>
+     * Example:
+     * {@code setComparator(Comparator.comparing(E::getPriority).thenComparing(E::toString));}
+     * <p>
+     * If {@code <E>} implements {@link java.lang.Comparable Comparable} you can automagically create a
+     * comparator with {@code setComparator(Comparator.naturalOrder());}.
+     *
+     * @param comparator A custom comparator.
+     */
+    public void setComparator(Comparator<E> comparator) {
+        Objects.requireNonNull(comparator, "A comparator cannot be null.");
+        this.comparator = comparator;
+    }
+
+    /**
+     * 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
+     * element matches {@code prefix}, returns the first of the matching elements (first according
+     * to {@link #comparator}). An element that is equal to {@code prefix} is always preferred.
+     *
+     * @param prefix The prefix to match.
+     * @return The best candidate (may be null)
+     */
+    public E findBestCandidate(String prefix) {
+        return elements.stream()
+            .filter(o -> o.toString().startsWith(prefix))
+            // an element equal to the prefix is always the best candidate
+            .min((x, y) -> x.toString().equals(prefix) ? -1 :
+                           y.toString().equals(prefix) ? 1 :
+                           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());
+        }
+    }
+}

Property changes on: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModel.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java	(working copy)
@@ -1,355 +1,16 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging.ac;
 
-import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.Transferable;
-import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
-import java.awt.im.InputContext;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.Locale;
-
-import javax.swing.ComboBoxModel;
-import javax.swing.DefaultComboBoxModel;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.JTextComponent;
-import javax.swing.text.PlainDocument;
-import javax.swing.text.StyleConstants;
-
 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.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;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
- * Auto-completing ComboBox.
+ * An auto-completing ComboBox.
+ *
  * @author guilhem.bonnefille@gmail.com
  * @since 272
+ * @deprecated Use the generic type {@link AutoCompComboBox} instead.  Eg.
+ *             {@code AutoCompComboBox<AutoCompletionItem>} or {@code AutoCompComboBox<String>}.
  */
-public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionItem> {
-
-    private boolean autocompleteEnabled = true;
-    private boolean locked;
-
-    private int maxTextLength = -1;
-    private boolean useFixedLocale;
-
-    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();
-            }
-        }
-    }
-
-    /**
-     * Auto-complete a JosmComboBox.
-     * <br>
-     * Inspired by <a href="http://www.orbital-computer.de/JComboBox">Thomas Bierhance example</a>.
-     */
-    class AutoCompletingComboBoxDocument extends PlainDocument {
-
-        @Override
-        public void remove(int offs, int len) throws BadLocationException {
-            try {
-                super.remove(offs, len);
-            } catch (IllegalArgumentException e) {
-                // IAE can happen with Devanagari script, see #15825
-                Logging.error(e);
-            }
-        }
-
-        @Override
-        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
-            // TODO get rid of code duplication w.r.t. AutoCompletingTextField.AutoCompletionDocument.insertString
-
-            if (maxTextLength > -1 && str.length() + getLength() > maxTextLength)
-                return;
-
-            super.insertString(offs, str, a);
-
-            if (locked)
-                return; // don't get in a loop
-
-            if (!autocompleteEnabled)
-                return;
-
-            // input method for non-latin characters (e.g. scim)
-            if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
-                return;
-
-            // if the cursor isn't at the end of the text we don't autocomplete.
-            // If a highlighted autocompleted suffix was present and we get here Swing has
-            // already removed it from the document. getLength() therefore doesn't include the autocompleted suffix.
-            if (offs + str.length() < getLength()) {
-                return;
-            }
-
-            String prefix = getText(0, getLength()); // the whole text after insertion
-
-            if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)
-                    && prefix.matches("^\\d+$"))
-                return;
-
-            autocomplete(prefix);
-
-            // save unix system selection (middle mouse paste)
-            Clipboard sysSel = ClipboardUtils.getSystemSelection();
-            if (sysSel != null) {
-                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
-                if (old != null) {
-                    sysSel.setContents(old, null);
-                }
-            }
-        }
-    }
-
-    /**
-     * Creates a <code>AutoCompletingComboBox</code> with a default prototype display value.
-     */
-    public AutoCompletingComboBox() {
-        this("Foo");
-    }
-
-    /**
-     * Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value.
-     * @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once
-     *                  before displaying a scroll bar. It also affects the initial width of the combo box.
-     * @since 5520
-     */
-    public AutoCompletingComboBox(String prototype) {
-        super(new AutoCompletionItem(prototype));
-        final JTextComponent editorComponent = this.getEditorComponent();
-        editorComponent.setDocument(new AutoCompletingComboBoxDocument());
-        editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
-    }
-
-    /**
-     * Autocomplete a string.
-     * <p>
-     * Look in the model for an item whose true prefix matches the string. If
-     * found, set the editor to the item and select the item in the model too.
-     *
-     * @param prefix The prefix to autocomplete.
-     */
-    private void autocomplete(String prefix) {
-        // candidate item for autocomplete
-        AutoCompletionItem item = findBestCandidate(prefix);
-        if (item != null) {
-            try {
-                locked = true;
-                setSelectedItem(item);
-                getEditor().setItem(item);
-                // select the autocompleted suffix in the editor
-                getEditorComponent().select(prefix.length(), item.getValue().length());
-            } finally {
-                locked = false;
-            }
-        }
-    }
-
-    /**
-     * Find the best candidate for autocompletion.
-     * @param prefix The true prefix to match.
-     * @return The best candidate (may be null)
-     */
-    private AutoCompletionItem findBestCandidate(String prefix) {
-        ComboBoxModel<AutoCompletionItem> model = getModel();
-        AutoCompletionItem bestCandidate = null;
-        for (int i = 0, n = model.getSize(); i < n; i++) {
-            AutoCompletionItem currentItem = model.getElementAt(i);
-            // the "same" string is always the best candidate, but it is of
-            // no use for autocompletion
-            if (currentItem.getValue().equals(prefix))
-                return null;
-            if (currentItem.getValue().startsWith(prefix)
-            && (bestCandidate == null || currentItem.getPriority().compareTo(bestCandidate.getPriority()) > 0)) {
-                bestCandidate = currentItem;
-            }
-        }
-        return bestCandidate;
-    }
-
-    /**
-     * Sets the maximum text length.
-     * @param length the maximum text length in number of characters
-     */
-    public void setMaxTextLength(int length) {
-        this.maxTextLength = length;
-    }
-
-    /**
-     * Selects a given item in the ComboBox model
-     * @param item the item of type AutoCompletionItem, String or null
-     * @param disableAutoComplete if true, autocomplete {@linkplain #setAutocompleteEnabled is disabled} during the operation
-     * @since 15885
-     */
-    public void setSelectedItem(Object item, final boolean disableAutoComplete) {
-        final boolean previousState = isAutocompleteEnabled();
-        if (disableAutoComplete) {
-            // disable autocomplete to prevent unnecessary actions in AutoCompletingComboBoxDocument#insertString
-            setAutocompleteEnabled(false);
-        }
-        setSelectedItem(item);
-        setAutocompleteEnabled(previousState);
-    }
-
-    /**
-     * Sets the items of the combobox to the given {@code String}s in reversed order (last element first).
-     * @param elems String items
-     */
-    public void setPossibleItems(Collection<String> elems) {
-        DefaultComboBoxModel<AutoCompletionItem> model = (DefaultComboBoxModel<AutoCompletionItem>) this.getModel();
-        Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
-        model.removeAllElements();
-        for (String elem : elems) {
-            model.addElement(new AutoCompletionItem(elem, AutoCompletionPriority.UNKNOWN));
-        }
-        this.setSelectedItem(null);
-        this.setSelectedItem(oldValue, true);
-    }
-
-    /**
-     * Sets the items of the combobox to the given {@code String}s in top down order.
-     * @param elems Collection of String items (is not changed)
-     * @since 15011
-     */
-    public void setPossibleItemsTopDown(Collection<String> elems) {
-        // We have to reverse the history, because ComboBoxHistory will reverse it again in addElement()
-        LinkedList<String> reversed = new LinkedList<>(elems);
-        Collections.reverse(reversed);
-        setPossibleItems(reversed);
-    }
-
-    /**
-     * Sets the items of the combobox to the given {@code AutoCompletionItem}s.
-     * @param elems AutoCompletionItem items
-     * @since 12859
-     */
-    public void setPossibleAcItems(Collection<AutoCompletionItem> elems) {
-        DefaultComboBoxModel<AutoCompletionItem> model = (DefaultComboBoxModel<AutoCompletionItem>) this.getModel();
-        Object oldValue = getSelectedItem();
-        Object editorOldValue = this.getEditor().getItem();
-        model.removeAllElements();
-        for (AutoCompletionItem elem : elems) {
-            model.addElement(elem);
-        }
-        setSelectedItem(oldValue);
-        this.getEditor().setItem(editorOldValue);
-    }
-
-    /**
-     * Determines if autocompletion is enabled.
-     * @return {@code true} if autocompletion is enabled, {@code false} otherwise.
-     */
-    public final boolean isAutocompleteEnabled() {
-        return autocompleteEnabled;
-    }
-
-    /**
-     * Sets whether the autocompletion is enabled
-     * @param autocompleteEnabled {@code true} to enable autocompletion
-     * @since 15567 (visibility)
-     */
-    public void setAutocompleteEnabled(boolean autocompleteEnabled) {
-        this.autocompleteEnabled = autocompleteEnabled;
-    }
-
-    /**
-     * If the locale is fixed, English keyboard layout will be used by default for this combobox
-     * all other components can still have different keyboard layout selected
-     * @param f fixed locale
-     */
-    public void setFixedLocale(boolean f) {
-        useFixedLocale = f;
-        if (useFixedLocale) {
-            Locale oldLocale = privateInputContext.getLocale();
-            Logging.info("Using English input method");
-            if (!privateInputContext.selectInputMethod(new Locale("en", "US"))) {
-                // Unable to use English keyboard layout, disable the feature
-                Logging.warn("Unable to use English input method");
-                useFixedLocale = false;
-                if (oldLocale != null) {
-                    Logging.info("Restoring input method to " + oldLocale);
-                    if (!privateInputContext.selectInputMethod(oldLocale)) {
-                        Logging.warn("Unable to restore input method to " + oldLocale);
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    public InputContext getInputContext() {
-        if (useFixedLocale) {
-            return privateInputContext;
-        }
-        return super.getInputContext();
-    }
-
-    /**
-     * Returns the edited item with whitespaces removed
-     * @return the edited item with whitespaces removed
-     * @since 15835
-     */
-    public String getEditItem() {
-        return Utils.removeWhiteSpaces(getEditor().getItem().toString());
-    }
-
-    /**
-     * Returns the selected item or the edited item as string
-     * @return the selected item or the edited item as string
-     * @see #getSelectedItem()
-     * @see #getEditItem()
-     * @since 15835
-     */
-    public String getSelectedOrEditItem() {
-        final Object selectedItem = getSelectedItem();
-        if (selectedItem instanceof AutoCompletionItem) {
-            return ((AutoCompletionItem) selectedItem).getValue();
-        } else if (selectedItem instanceof String) {
-            return (String) selectedItem;
-        } else {
-            return getEditItem();
-        }
-    }
+@Deprecated
+public class AutoCompletingComboBox extends AutoCompComboBox<AutoCompletionItem> {
 }
Index: src/org/openstreetmap/josm/gui/widgets/ComboBoxHistory.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/ComboBoxHistory.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/widgets/ComboBoxHistory.java	(nonexistent)
@@ -1,122 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.widgets;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import javax.swing.DefaultComboBoxModel;
-
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
-
-/**
- * A data model for {@link HistoryComboBox}
- */
-class ComboBoxHistory extends DefaultComboBoxModel<AutoCompletionItem> implements Iterable<AutoCompletionItem> {
-
-    private final int maxSize;
-
-    /**
-     * Constructs a {@code ComboBoxHistory} keeping track of {@code maxSize} items
-     * @param size the history size
-     */
-    ComboBoxHistory(int size) {
-        maxSize = size;
-    }
-
-    /**
-     * Adds or moves an element to the top of the history
-     * @param s the element to add
-     */
-    public void addElement(String s) {
-        addElement(new AutoCompletionItem(s));
-    }
-
-    /**
-     * Adds or moves an element to the top of the history
-     * @param o the element to add
-     */
-    @Override
-    public void addElement(AutoCompletionItem o) {
-        String newEntry = o.getValue();
-
-        boolean alreadyAdded = false;
-        // if history contains this object already, delete it,
-        // so that it looks like a move to the top
-        for (int i = 0; i < getSize(); i++) {
-            String oldEntry = getElementAt(i).getValue();
-            if (oldEntry.equals(newEntry)) {
-                if (i == 0) {
-                    alreadyAdded = true;
-                    break;
-                } else {
-                    removeElementAt(i);
-                }
-            }
-        }
-
-        if (!alreadyAdded) {
-            // insert element at the top
-            insertElementAt(o, 0);
-        }
-
-        // remove an element, if the history gets too large
-        if (getSize() > maxSize) {
-            removeElementAt(getSize()-1);
-        }
-
-        // set selected item to the one just added
-        setSelectedItem(o);
-    }
-
-    @Override
-    public Iterator<AutoCompletionItem> iterator() {
-        return new Iterator<AutoCompletionItem>() {
-
-            private int position = -1;
-
-            @Override
-            public void remove() {
-                removeElementAt(position);
-            }
-
-            @Override
-            public boolean hasNext() {
-                return position < getSize()-1 && getSize() > 0;
-            }
-
-            @Override
-            public AutoCompletionItem next() {
-                if (!hasNext())
-                    throw new NoSuchElementException();
-                position++;
-                return getElementAt(position);
-            }
-        };
-    }
-
-    /**
-     * {@link javax.swing.DefaultComboBoxModel#removeAllElements() Removes all items}
-     * and {@link ComboBoxHistory#addElement(String) adds} the given items.
-     * @param items the items to set
-     */
-    public void setItemsAsString(List<String> items) {
-        removeAllElements();
-        for (int i = items.size()-1; i >= 0; i--) {
-            addElement(items.get(i));
-        }
-    }
-
-    /**
-     * Returns the {@link AutoCompletionItem} items as strings
-     * @return a list of strings
-     */
-    public List<String> asStringList() {
-        List<String> list = new ArrayList<>(maxSize);
-        for (AutoCompletionItem item : this) {
-            list.add(item.getValue());
-        }
-        return list;
-    }
-}

Property changes on: src/org/openstreetmap/josm/gui/widgets/ComboBoxHistory.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/widgets/HistoryComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/HistoryComboBox.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/widgets/HistoryComboBox.java	(working copy)
@@ -3,82 +3,47 @@
 
 import java.util.List;
 
-import javax.swing.text.JTextComponent;
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBox;
 
-import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
-import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox;
-import org.openstreetmap.josm.spi.preferences.Config;
-
 /**
- * An {@link AutoCompletingComboBox} which keeps a history
+ * A History ComboBox
+ * <p>
+ * A HistoryComboBox is an {@link AutoCompComboBox} specialized in {@code String}s.
  */
-public class HistoryComboBox extends AutoCompletingComboBox {
-    private final ComboBoxHistory model;
+public class HistoryComboBox extends AutoCompComboBox<String> {
 
     /**
-     * The default size of the search history.
-     */
-    public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
-
-    /**
      * Constructs a new {@code HistoryComboBox}.
      */
     public HistoryComboBox() {
-        int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
-        model = new ComboBoxHistory(maxsize);
-        setModel(model);
-        setEditable(true);
+        super(new HistoryComboBoxModel());
+        setPrototypeDisplayValue("dummy");
     }
 
-    /**
-     * Returns the text contained in this component
-     * @return the text
-     * @see JTextComponent#getText()
-     */
-    public String getText() {
-        return getEditorComponent().getText();
+    @Override
+    public HistoryComboBoxModel getModel() {
+        return (HistoryComboBoxModel) dataModel;
     }
 
     /**
-     * Sets the text of this component to the specified text
-     * @param value the text to set
-     * @see JTextComponent#setText(java.lang.String)
+     * Adds the item in the editor to the top of the history. If the item is already present, don't
+     * add another but move it to the top. The item is then selected.
      */
-    public void setText(String value) {
-        setAutocompleteEnabled(false);
-        getEditorComponent().setText(value);
-        setAutocompleteEnabled(true);
-    }
-
-    /**
-     * Adds or moves the current element to the top of the history
-     * @see ComboBoxHistory#addElement(java.lang.String)
-     */
     public void addCurrentItemToHistory() {
-        Object item = getEditor().getItem();
-        // This avoids instantiating multiple AutoCompletionItems
-        if (item instanceof AutoCompletionItem) {
-            model.addElement((AutoCompletionItem) item);
-        } else {
-            model.addElement(item.toString());
-        }
+        String newItem = getModel().addTopElement(getEditor().getItem().toString());
+        getModel().setSelectedItem(newItem);
     }
 
     /**
-     * Sets the elements of the ComboBox to the given items
-     * @param history the items to set
-     * @see ComboBoxHistory#setItemsAsString(java.util.List)
-     */
-    public void setHistory(List<String> history) {
-        model.setItemsAsString(history);
-    }
-
-    /**
      * Returns the items as strings
      * @return the items as strings
-     * @see ComboBoxHistory#asStringList()
+     * @deprecated Has been moved to the model, where it belongs. Use
+     *     {@link HistoryComboBoxModel#asStringList} instead.  Probably you want to use
+     *     {@link HistoryComboBoxModel.Preferences#load} and
+     *     {@link HistoryComboBoxModel.Preferences#save}.
      */
+    @Deprecated
     public List<String> getHistory() {
-        return model.asStringList();
+        return getModel().asStringList();
     }
 }
Index: src/org/openstreetmap/josm/gui/widgets/HistoryComboBoxModel.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/HistoryComboBoxModel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/widgets/HistoryComboBoxModel.java	(working copy)
@@ -0,0 +1,55 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel;
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * A data model for the {@link HistoryComboBox}.
+ * <p>
+ * This model is an {@link AutoCompComboBoxModel} specialized in {@code String}s. It offers
+ * convenience functions to serialize to and from the JOSM preferences.
+ *
+ * @since xxx
+ */
+public class HistoryComboBoxModel extends AutoCompComboBoxModel<String> {
+
+    HistoryComboBoxModel() {
+        // The user's preference for max. number of items in histories.
+        setSize(Config.getPref().getInt("search.history-size", 15));
+    }
+
+    /**
+     * Adds strings to the model.
+     * <p>
+     * Strings are added only until the max. history size is reached.
+     *
+     * @param strings the strings to add
+     */
+    public void addAllStrings(List<String> strings) {
+        strings.forEach(s -> addElement(s));
+    }
+
+    /**
+     * Gets all items in the history as a list of strings.
+     *
+     * @return the items in the history
+     */
+    public List<String> asStringList() {
+        List<String> list = new ArrayList<>(getSize());
+        this.forEach(item -> list.add(item));
+        return list;
+    }
+
+    /**
+     * Gets a preference loader and saver for this model.
+     *
+     * @return the instance
+     */
+    public Preferences prefs() {
+        return prefs(x -> x, x -> x);
+    }
+}

Property changes on: src/org/openstreetmap/josm/gui/widgets/HistoryComboBoxModel.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java
===================================================================
--- src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(revision 18170)
+++ src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java	(working copy)
@@ -109,6 +109,26 @@
     }
 
     /**
+     * Returns the text in the combobox editor.
+     * @return the text
+     * @see JTextComponent#getText
+     * @since xxx
+     */
+    public String getText() {
+        return getEditorComponent().getText();
+    }
+
+    /**
+     * Sets the text in the combobox editor.
+     * @param value the text to set
+     * @see JTextComponent#setText
+     * @since xxx
+     */
+    public void setText(String value) {
+        getEditorComponent().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.
@@ -163,13 +183,13 @@
                 .orElse(null);
     }
 
-    protected final void init(E prototype) {
-        init(prototype, true);
-    }
-
-    protected final void init(E prototype, boolean registerPropertyChangeListener) {
+    /**
+     * Set the prototypeCellValue property and calculate the height of the dropdown.
+     */
+    @Override
+    public void setPrototypeDisplayValue(E prototype) {
         if (prototype != null) {
-            setPrototypeDisplayValue(prototype);
+            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
@@ -188,6 +208,14 @@
             }
             setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
         }
+    }
+
+    protected final void init(E prototype) {
+        init(prototype, true);
+    }
+
+    protected final void init(E prototype, boolean registerPropertyChangeListener) {
+        setPrototypeDisplayValue(prototype);
         // Handle text contextual menus for editable comboboxes
         if (registerPropertyChangeListener) {
             addPropertyChangeListener("editable", handler);
Index: test/unit/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanelTest.java	(revision 18170)
+++ test/unit/org/openstreetmap/josm/gui/io/BasicUploadSettingsPanelTest.java	(working copy)
@@ -1,8 +1,15 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.io;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
+import java.util.Arrays;
+import java.util.List;
+
+import org.openstreetmap.josm.spi.preferences.Config;
+
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 
 import org.junit.jupiter.api.Test;
@@ -17,6 +24,42 @@
      */
     @Test
     void testBasicUploadSettingsPanel() {
-        assertNotNull(new BasicUploadSettingsPanel(new ChangesetCommentModel(), new ChangesetCommentModel(), new ChangesetReviewModel()));
+        assertNotNull(new BasicUploadSettingsPanel(new UploadDialogModel()));
     }
+
+    private static void doTestGetLastChangesetTagFromHistory(String historyKey, List<String> def) {
+        Config.getPref().putList(historyKey, null);
+        Config.getPref().putInt(BasicUploadSettingsPanel.COMMENT_LAST_USED_KEY, 0);
+        Config.getPref().putInt(BasicUploadSettingsPanel.COMMENT_MAX_AGE_KEY, 30);
+        assertNull(BasicUploadSettingsPanel.getLastChangesetTagFromHistory(historyKey, def));          // age NOK (history empty)
+
+        Config.getPref().putList(historyKey, Arrays.asList("foo", "bar"));
+        assertNull(BasicUploadSettingsPanel.getLastChangesetTagFromHistory(historyKey, def));          // age NOK (history not empty)
+
+        Config.getPref().putLong(BasicUploadSettingsPanel.COMMENT_LAST_USED_KEY, System.currentTimeMillis() / 1000);
+        assertEquals("foo", BasicUploadSettingsPanel.getLastChangesetTagFromHistory(historyKey, def)); // age OK, history not empty
+
+        Config.getPref().putList(historyKey, null);
+        assertEquals(def.get(0), BasicUploadSettingsPanel.getLastChangesetTagFromHistory(historyKey, def));   // age OK, history empty
+    }
+
+    /**
+     * Test of {@link BasicUploadSettingsPanel#getLastChangesetTagFromHistory} method.
+     */
+    @Test
+    void testGetLastChangesetCommentFromHistory() {
+        doTestGetLastChangesetTagFromHistory(
+                BasicUploadSettingsPanel.COMMENT_HISTORY_KEY,
+                Arrays.asList("baz", "quux"));
+    }
+
+    /**
+     * Test of {@link BasicUploadSettingsPanel#getLastChangesetTagFromHistory} method.
+     */
+    @Test
+    void testGetLastChangesetSourceFromHistory() {
+        doTestGetLastChangesetTagFromHistory(
+                BasicUploadSettingsPanel.SOURCE_HISTORY_KEY,
+                BasicUploadSettingsPanel.getDefaultSources());
+    }
 }
Index: test/unit/org/openstreetmap/josm/gui/io/ChangesetCommentModelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/ChangesetCommentModelTest.java	(revision 18170)
+++ test/unit/org/openstreetmap/josm/gui/io/ChangesetCommentModelTest.java	(nonexistent)
@@ -1,55 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.io;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link ChangesetCommentModel} class.
- */
-class ChangesetCommentModelTest {
-
-    /**
-     * Setup tests
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
-
-    /**
-     * Test of {@link ChangesetCommentModel#findHashTags}.
-     */
-    @Test
-    void testFindHashTags() {
-        ChangesetCommentModel model = new ChangesetCommentModel();
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment(" ");
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment(" #");
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment(" # ");
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment(" https://example.com/#map ");
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment("#59606086");
-        assertEquals(Collections.emptyList(), model.findHashTags());
-        model.setComment(" #foo ");
-        assertEquals(Arrays.asList("#foo"), model.findHashTags());
-        model.setComment(" #foo #bar baz");
-        assertEquals(Arrays.asList("#foo", "#bar"), model.findHashTags());
-        model.setComment(" #foo, #bar, baz");
-        assertEquals(Arrays.asList("#foo", "#bar"), model.findHashTags());
-        model.setComment(" #foo; #bar; baz");
-        assertEquals(Arrays.asList("#foo", "#bar"), model.findHashTags());
-        model.setComment("#hotosm-project-4773 #DRONEBIRD #OsakaQuake2018 #AOYAMAVISION");
-        assertEquals(Arrays.asList("#hotosm-project-4773", "#DRONEBIRD", "#OsakaQuake2018", "#AOYAMAVISION"), model.findHashTags());
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/io/ChangesetCommentModelTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/io/ChangesetManagementPanelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/ChangesetManagementPanelTest.java	(revision 18170)
+++ test/unit/org/openstreetmap/josm/gui/io/ChangesetManagementPanelTest.java	(working copy)
@@ -4,6 +4,7 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.junit.jupiter.api.Test;
+
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
 
 /**
@@ -16,6 +17,6 @@
      */
     @Test
     void testChangesetManagementPanel() {
-        assertNotNull(new ChangesetManagementPanel(new ChangesetCommentModel()));
+        assertNotNull(new ChangesetManagementPanel(new UploadDialogModel()));
     }
 }
Index: test/unit/org/openstreetmap/josm/gui/io/TagSettingsPanelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/TagSettingsPanelTest.java	(revision 18170)
+++ test/unit/org/openstreetmap/josm/gui/io/TagSettingsPanelTest.java	(nonexistent)
@@ -1,31 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.gui.io;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests of {@link TagSettingsPanel} class.
- */
-class TagSettingsPanelTest {
-
-    /**
-     * Setup tests
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences();
-
-    /**
-     * Test of {@link TagSettingsPanel#TagSettingsPanel}.
-     */
-    @Test
-    void testTagSettingsPanel() {
-        assertNotNull(new TagSettingsPanel(new ChangesetCommentModel(), new ChangesetCommentModel(), new ChangesetReviewModel()));
-    }
-}

Property changes on: test/unit/org/openstreetmap/josm/gui/io/TagSettingsPanelTest.java
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/io/UploadDialogModelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/UploadDialogModelTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/io/UploadDialogModelTest.java	(working copy)
@@ -0,0 +1,68 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.Test;
+
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Unit tests of {@link UploadDialogModel} class.
+ */
+
+public class UploadDialogModelTest {
+    /**
+     * Setup tests
+     */
+    @RegisterExtension
+    public JOSMTestRules test = new JOSMTestRules().preferences().main();
+
+    /**
+     * Test of {@link UploadDialogModel}.
+     */
+    @Test
+    void testUploadDialogModel() {
+        assertNotNull(new UploadDialogModel());
+    }
+
+    @Test
+    void testFindHashTags() {
+        UploadDialogModel model = new UploadDialogModel();
+
+        assertNull(model.findHashTags(" "));
+        assertNull(model.findHashTags(" #"));
+        assertNull(model.findHashTags(" # "));
+        assertNull(model.findHashTags(" https://example.com/#map "));
+        assertNull(model.findHashTags("#59606086"));
+        assertEquals("#foo", model.findHashTags(" #foo "));
+        assertEquals("#foo;#bar", model.findHashTags(" #foo #bar baz"));
+        assertEquals("#foo;#bar", model.findHashTags(" #foo, #bar, baz"));
+        assertEquals("#foo;#bar", model.findHashTags(" #foo; #bar; baz"));
+        assertEquals("#hotosm-project-4773;#DRONEBIRD;#OsakaQuake2018;#AOYAMAVISION",
+            model.findHashTags("#hotosm-project-4773 #DRONEBIRD #OsakaQuake2018 #AOYAMAVISION"));
+    }
+
+    @Test
+    void testCommentWithHashtags() {
+        UploadDialogModel model = new UploadDialogModel();
+        model.add("comment", "comment with a #hashtag");
+        assertEquals("#hashtag", model.getValue("hashtags"));
+    }
+
+    @Test
+    void testGetCommentWithDataSetHashTag() {
+        assertEquals("", UploadDialogModel.addHashTagsFromDataSet(null, null));
+        DataSet ds = new DataSet();
+        assertEquals("foo", UploadDialogModel.addHashTagsFromDataSet("foo", ds));
+        ds.getChangeSetTags().put("hashtags", "bar");
+        assertEquals("foo #bar", UploadDialogModel.addHashTagsFromDataSet("foo", ds));
+        ds.getChangeSetTags().put("hashtags", "bar;baz;#bar");
+        assertEquals("foo #bar #baz", UploadDialogModel.addHashTagsFromDataSet("foo", ds));
+    }
+
+}

Property changes on: test/unit/org/openstreetmap/josm/gui/io/UploadDialogModelTest.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/io/UploadDialogTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/io/UploadDialogTest.java	(revision 18170)
+++ test/unit/org/openstreetmap/josm/gui/io/UploadDialogTest.java	(working copy)
@@ -12,13 +12,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
 
 import javax.swing.JOptionPane;
 
 import org.junit.jupiter.api.Test;
 import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.io.UploadDialog.UploadAction;
 import org.openstreetmap.josm.io.UploadStrategySpecification;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -81,11 +79,6 @@
         public Map<String, String> getTags(boolean keepEmpty) {
             return new ConcurrentHashMap<>();
         }
-
-        @Override
-        public void forceUpdateActiveField() {
-            // Do nothing
-        }
     }
 
     /**
@@ -115,41 +108,6 @@
         assertTrue(UploadDialog.UploadAction.isUploadCommentTooShort("\u0860"));
     }
 
-    private static void doTestGetLastChangesetTagFromHistory(String historyKey, Supplier<String> methodToTest, String def) {
-        Config.getPref().putList(historyKey, null);
-        Config.getPref().putInt(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0);
-        Config.getPref().putInt(BasicUploadSettingsPanel.HISTORY_MAX_AGE_KEY, 30);
-        assertNull(methodToTest.get());          // age NOK (history empty)
-        Config.getPref().putList(historyKey, Arrays.asList("foo"));
-        assertNull(methodToTest.get());          // age NOK (history not empty)
-        Config.getPref().putLong(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, System.currentTimeMillis() / 1000);
-        assertEquals("foo", methodToTest.get()); // age OK, history not empty
-        Config.getPref().putList(historyKey, null);
-        assertEquals(def, methodToTest.get());   // age OK, history empty
-    }
-
-    /**
-     * Test of {@link UploadDialog#getLastChangesetCommentFromHistory} method.
-     */
-    @Test
-    void testGetLastChangesetCommentFromHistory() {
-        doTestGetLastChangesetTagFromHistory(
-                BasicUploadSettingsPanel.HISTORY_KEY,
-                UploadDialog::getLastChangesetCommentFromHistory,
-                null);
-    }
-
-    /**
-     * Test of {@link UploadDialog#getLastChangesetSourceFromHistory} method.
-     */
-    @Test
-    void testGetLastChangesetSourceFromHistory() {
-        doTestGetLastChangesetTagFromHistory(
-                BasicUploadSettingsPanel.SOURCE_HISTORY_KEY,
-                UploadDialog::getLastChangesetSourceFromHistory,
-                BasicUploadSettingsPanel.getDefaultSources().get(0));
-    }
-
     private static void doTestValidateUploadTag(String prefix) {
         List<String> def = Collections.emptyList();
         Config.getPref().putList(prefix + ".mandatory-terms", null);
@@ -185,15 +143,4 @@
         doTestValidateUploadTag("upload.comment");
         doTestValidateUploadTag("upload.source");
     }
-
-    @Test
-    void testGetCommentWithDataSetHashTag() {
-        assertEquals("", UploadDialog.getCommentWithDataSetHashTag(null, null));
-        DataSet ds = new DataSet();
-        assertEquals("foo", UploadDialog.getCommentWithDataSetHashTag("foo", ds));
-        ds.getChangeSetTags().put("hashtags", "bar");
-        assertEquals("foo #bar", UploadDialog.getCommentWithDataSetHashTag("foo", ds));
-        ds.getChangeSetTags().put("hashtags", "bar;baz;#bar");
-        assertEquals("foo #bar #baz", UploadDialog.getCommentWithDataSetHashTag("foo", ds));
-    }
 }
Index: test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModelTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModelTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModelTest.java	(working copy)
@@ -0,0 +1,100 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.search.SearchSetting;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionPriority;
+
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
+
+/**
+ * Test class for {@link AutoCompComboBoxModel}
+ */
+@FullPreferences
+class AutoCompComboBoxModelTest {
+
+    class TestData {
+        public String s;
+        public AutoCompletionItem ac;
+        public SearchSetting ss;
+
+        TestData(String s, AutoCompletionPriority p) {
+            this.s = s;
+            this.ss = new SearchSetting();
+            ss.text = s;
+            this.ac = new AutoCompletionItem(s, p);
+        }
+    }
+
+    // CHECKSTYLE.OFF: SingleSpaceSeparator
+    // CHECKSTYLE.OFF: ParenPad
+
+    Map<String, TestData> testData = new LinkedHashMap<String, TestData>() {{
+        put("a1",   new TestData("a1",   AutoCompletionPriority.UNKNOWN));
+        put("a2",   new TestData("a2",   AutoCompletionPriority.IS_IN_STANDARD));
+        put("a3",   new TestData("a3",   AutoCompletionPriority.IS_IN_DATASET));
+        put("a4",   new TestData("a4",   AutoCompletionPriority.IS_IN_STANDARD_AND_IN_DATASET));
+        put("b",    new TestData("b",    AutoCompletionPriority.UNKNOWN));
+        put("bcde", new TestData("bcde", AutoCompletionPriority.UNKNOWN));
+        put("bde",  new TestData("bde",  AutoCompletionPriority.UNKNOWN));
+        put("bdef", new TestData("bdef", AutoCompletionPriority.IS_IN_STANDARD_AND_IN_DATASET));
+    }};
+
+    @Test
+    void testAutoCompModel() {
+        assertNotNull(new AutoCompComboBoxModel<String>());
+        assertNotNull(new AutoCompComboBoxModel<SearchSetting>());
+        assertNotNull(new AutoCompComboBoxModel<AutoCompletionItem>());
+    }
+
+    @Test
+    void testAutoCompModelFindString() {
+        AutoCompComboBoxModel<String> model = new AutoCompComboBoxModel<>();
+        testData.forEach((k, v) -> model.addElement(v.s));
+
+        assertNull(model.findBestCandidate("bb"));
+        assertEquals("a1",   model.findBestCandidate("a" ));
+        assertEquals("b",    model.findBestCandidate("b" ));
+        assertEquals("bcde", model.findBestCandidate("bc"));
+        assertEquals("bde",  model.findBestCandidate("bd"));
+    }
+
+    @Test
+    void testAutoCompModelFindSearchSetting() {
+        AutoCompComboBoxModel<SearchSetting> model = new AutoCompComboBoxModel<>();
+        // Use the default Comparator (that compares on toString).
+        testData.forEach((k, v) -> model.addElement(v.ss));
+
+        assertNull(model.findBestCandidate("bb"));
+        // test for sameness (aka ==).  Some objects are expensive to copy, so we want to be able to
+        // round-trip an object thru the AutoCompComboBox without copying it.
+        assertSame(testData.get("a1"  ).ss, model.findBestCandidate("a" ));
+        assertSame(testData.get("b"   ).ss, model.findBestCandidate("b" ));
+        assertSame(testData.get("bcde").ss, model.findBestCandidate("bc"));
+        assertSame(testData.get("bde" ).ss, model.findBestCandidate("bd"));
+    }
+
+    @Test
+    void testAutoCompModelFindAutoCompletionItem() {
+        AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>();
+        // AutoCompletionItem implements Comparable. Build a Comparator from Comparable.
+        model.setComparator(Comparator.naturalOrder());
+        testData.forEach((k, v) -> model.addElement(v.ac));
+
+        assertNull(model.findBestCandidate("bb"));
+        assertSame(testData.get("a4"  ).ac, model.findBestCandidate("a" )); // higher prio than "a1"
+        assertSame(testData.get("b"   ).ac, model.findBestCandidate("b" ));
+        assertSame(testData.get("bcde").ac, model.findBestCandidate("bc"));
+        assertSame(testData.get("bdef").ac, model.findBestCandidate("bd")); // higher prio than "bde"
+    }
+}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxModelTest.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxTest.java	(working copy)
@@ -0,0 +1,21 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.tagging.ac;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
+
+/**
+ * Test class for {@link AutoCompletingComboBox}
+ */
+@FullPreferences
+class AutoCompComboBoxTest {
+
+    @Test
+    void testAutoCompletingComboBox() {
+        assertNotNull(new AutoCompComboBox<String>());
+        assertNotNull(new AutoCompComboBox<AutoCompletionItem>());
+    }
+}

Property changes on: test/unit/org/openstreetmap/josm/gui/tagging/ac/AutoCompComboBoxTest.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
