commit c12fc123f2111694e58c8a8a36320dbe38b0b15b
Author: Simon Legner <Simon.Legner@gmail.com>
Date:   Wed Dec 23 16:25:33 2015 +0100

    see #12224 - Dialog for "Search menu items"

diff --git a/src/org/openstreetmap/josm/gui/MainApplication.java b/src/org/openstreetmap/josm/gui/MainApplication.java
index 5198e66..64c6842 100644
--- a/src/org/openstreetmap/josm/gui/MainApplication.java
+++ b/src/org/openstreetmap/josm/gui/MainApplication.java
@@ -471,7 +471,6 @@ public class MainApplication extends Main {
                 splash.setVisible(false);
                 splash.dispose();
                 mainFrame.setVisible(true);
-                main.gettingStarted.requestFocusInWindow();
             }
         });
 
diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
index f996248..82ab5b7 100644
--- a/src/org/openstreetmap/josm/gui/MainMenu.java
+++ b/src/org/openstreetmap/josm/gui/MainMenu.java
@@ -6,32 +6,21 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trc;
 
 import java.awt.Component;
-import java.awt.DefaultFocusTraversalPolicy;
-import java.awt.Dimension;
 import java.awt.GraphicsEnvironment;
-import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.awt.event.KeyListener;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
-import javax.swing.Action;
-import javax.swing.Box;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JMenu;
 import javax.swing.JMenuBar;
 import javax.swing.JMenuItem;
 import javax.swing.JPopupMenu;
 import javax.swing.JSeparator;
-import javax.swing.JTextField;
 import javax.swing.KeyStroke;
-import javax.swing.MenuElement;
-import javax.swing.MenuSelectionManager;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
 import javax.swing.event.MenuEvent;
 import javax.swing.event.MenuListener;
 
@@ -124,6 +113,7 @@ import org.openstreetmap.josm.actions.audio.AudioSlowerAction;
 import org.openstreetmap.josm.actions.search.SearchAction;
 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
 import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
+import org.openstreetmap.josm.gui.dialogs.MenuItemSearchDialog;
 import org.openstreetmap.josm.gui.io.RecentlyOpenedFilesMenu;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.mappaint.MapPaintMenu;
@@ -131,7 +121,6 @@ import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSearchAction;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSearchPrimitiveDialog;
-import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
 import org.openstreetmap.josm.tools.Shortcut;
 
 /**
@@ -403,11 +392,6 @@ public class MainMenu extends JMenuBar {
     public final DialogsToggleAction dialogsToggleAction = new DialogsToggleAction();
     public FullscreenToggleAction fullscreenToggleAction;
 
-    /**
-     * Popup menu to display menu items search result.
-     */
-    private final JPopupMenu searchResultsMenu = new JPopupMenu();
-
     /** this menu listener hides unnecessary JSeparators in a menu list but does not remove them.
      * If at a later time the separators are required, they will be made visible again. Intended
      * usage is make menus not look broken if separators are used to group the menu and some of
@@ -440,7 +424,7 @@ public class MainMenu extends JMenuBar {
     };
 
     /**
-     * @return the default position of tnew top-level menus
+     * @return the default position of new top-level menus
      * @since 6088
      */
     public int getDefaultMenuPos() {
@@ -809,6 +793,8 @@ public class MainMenu extends JMenuBar {
             }
         });
 
+        helpMenu.add(new MenuItemSearchDialog.Action());
+        helpMenu.addSeparator();
         helpMenu.add(statusreport);
         helpMenu.add(reportbug);
         helpMenu.addSeparator();
@@ -817,18 +803,6 @@ public class MainMenu extends JMenuBar {
         helpMenu.add(help).setAccelerator(Shortcut.registerShortcut("system:help", tr("Help"), KeyEvent.VK_F1,
                 Shortcut.DIRECT).getKeyStroke());
         add(helpMenu, about);
-        add(Box.createHorizontalGlue());
-        final DisableShortcutsOnFocusGainedTextField searchField = createSearchField();
-        add(searchField);
-
-        // Do not let search field take the focus automatically
-        setFocusTraversalPolicyProvider(true);
-        setFocusTraversalPolicy(new DefaultFocusTraversalPolicy() {
-            @Override
-            protected boolean accept(Component aComponent) {
-                return super.accept(aComponent) && !searchField.equals(aComponent);
-            }
-        });
 
         windowMenu.addMenuListener(menuSeparatorHandler);
 
@@ -847,51 +821,19 @@ public class MainMenu extends JMenuBar {
     }
 
     /**
-     * Create search field.
-     * @return the search field
-     */
-    private DisableShortcutsOnFocusGainedTextField createSearchField() {
-        DisableShortcutsOnFocusGainedTextField searchField = new DisableShortcutsOnFocusGainedTextField() {
-            @Override
-            public Dimension getPreferredSize() {
-                // JMenuBar uses a BoxLayout and it doesn't seem possible to specify a size factor,
-                // so compute the preferred size dynamically
-                return new Dimension(Math.min(200, Math.max(25, getMaximumAvailableWidth())),
-                        helpMenu.getPreferredSize().height);
-            }
-        };
-        Shortcut searchFieldShortcut = Shortcut.registerShortcut("menu:search-field", tr("Search menu items"), KeyEvent.VK_R, Shortcut.MNEMONIC);
-        searchFieldShortcut.setFocusAccelerator(searchField);
-        searchField.setEditable(true);
-        searchField.setMaximumSize(new Dimension(200, helpMenu.getPreferredSize().height));
-        searchField.setHint(tr("Search menu items"));
-        searchField.setToolTipText(Main.platform.makeTooltip(tr("Search menu items"), searchFieldShortcut));
-        searchField.addKeyListener(new SearchFieldKeyListener());
-        searchField.getDocument().addDocumentListener(new SearchFieldTextListener(this, searchField));
-        return searchField;
-    }
-
-    /**
      * Search main menu for items with {@code textToFind} in title.
      * @param textToFind The text to find
+     * @param skipPresets whether to skip the {@link #presetsMenu} in the search
      * @return not null list of found menu items.
      */
-    private List<JMenuItem> findMenuItems(String textToFind) {
-        // Explicitely use default locale in this case, because we're looking for translated strings
+    public List<JMenuItem> findMenuItems(String textToFind, boolean skipPresets) {
+        // Explicitly use default locale in this case, because we're looking for translated strings
         textToFind = textToFind.toLowerCase(Locale.getDefault());
         List<JMenuItem> result = new ArrayList<>();
-
-        // Iterate over main menus
-        for (MenuElement menuElement : getSubElements()) {
-            if (!(menuElement instanceof JMenu)) continue;
-
-            JMenu mainMenuItem = (JMenu) menuElement;
-            if (mainMenuItem.getAction() != null && mainMenuItem.getText().toLowerCase(Locale.getDefault()).contains(textToFind)) {
-                result.add(mainMenuItem);
+        for (int i = 0; i < getMenuCount(); i++) {
+            if (getMenu(i) != null && (!skipPresets || presetsMenu != getMenu(i))) {
+                findMenuItems(getMenu(i), textToFind, result);
             }
-
-            //Search recursively
-            findMenuItems(mainMenuItem, textToFind, result);
         }
         return result;
     }
@@ -901,14 +843,14 @@ public class MainMenu extends JMenuBar {
      * contains {@code textToFind} it's appended to result.
      * @param menu menu in which search will be performed
      * @param textToFind The text to find
-     * @param result resulting list ofmenu items
+     * @param result resulting list of menu items
      */
     private static void findMenuItems(final JMenu menu, final String textToFind, final List<JMenuItem> result) {
         for (int i = 0; i < menu.getItemCount(); i++) {
             JMenuItem menuItem = menu.getItem(i);
             if (menuItem == null) continue;
 
-            // Explicitely use default locale in this case, because we're looking for translated strings
+            // Explicitly use default locale in this case, because we're looking for translated strings
             if (menuItem.getAction() != null && menuItem.getText().toLowerCase(Locale.getDefault()).contains(textToFind)) {
                 result.add(menuItem);
             }
@@ -970,110 +912,4 @@ public class MainMenu extends JMenuBar {
         }
     }
 
-    /**
-     * This listener is designed to handle ENTER key pressed in menu search field.
-     * When user presses Enter key then selected item of "searchResultsMenu" is triggered.
-     */
-    private static class SearchFieldKeyListener implements KeyListener {
-
-        @Override
-        public void keyPressed(KeyEvent e) {
-            if (e.getKeyCode() == KeyEvent.VK_ENTER) {
-                // On ENTER selected menu item must be triggered
-                MenuElement[] selection = MenuSelectionManager.defaultManager().getSelectedPath();
-                if (selection.length > 1) {
-                    MenuElement selectedElement = selection[selection.length-1];
-                    if (selectedElement instanceof JMenuItem) {
-                        JMenuItem selectedItem = (JMenuItem) selectedElement;
-                        Action menuAction = selectedItem.getAction();
-                        menuAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null));
-                        if (Main.isDebugEnabled()) {
-                            Main.debug(getClass().getName()+" consuming event "+e);
-                        }
-                        e.consume();
-                    }
-                }
-            }
-        }
-
-        @Override
-        public void keyTyped(KeyEvent e) {
-            // Not used
-        }
-
-        @Override
-        public void keyReleased(KeyEvent e) {
-            // Not used
-        }
-    }
-
-    private class SearchFieldTextListener implements DocumentListener {
-        private final JTextField searchField;
-        private final MainMenu mainMenu;
-        private String currentSearchText;
-
-        SearchFieldTextListener(MainMenu mainMenu, JTextField searchField) {
-            this.mainMenu = mainMenu;
-            this.searchField = searchField;
-        }
-
-        @Override
-        public void insertUpdate(DocumentEvent e) {
-            doSearch(searchField.getText());
-        }
-
-        @Override
-        public void removeUpdate(DocumentEvent e) {
-            doSearch(searchField.getText());
-        }
-
-        @Override
-        public void changedUpdate(DocumentEvent e) {
-            doSearch(searchField.getText());
-        }
-
-        //TODO: perform some delay (maybe 200 ms) before actual searching.
-        void doSearch(String searchTerm) {
-            // Explicitely use default locale in this case, because we're looking for translated strings
-            searchTerm = searchTerm.trim().toLowerCase(Locale.getDefault());
-
-            if (searchTerm.equals(currentSearchText)) {
-                return;
-            }
-            currentSearchText = searchTerm;
-            if (searchTerm.isEmpty()) {
-                // No text to search
-                hideMenu();
-                return;
-            }
-
-            List<JMenuItem> searchResult = mainMenu.findMenuItems(currentSearchText);
-            if (searchResult.isEmpty()) {
-                // Nothing found
-                hideMenu();
-                return;
-            }
-
-            if (searchResult.size() > 20) {
-                // Too many items found...
-                searchResult = searchResult.subList(0, 20);
-            }
-
-            // Update Popup menu
-            searchResultsMenu.removeAll();
-            for (JMenuItem foundItem : searchResult) {
-                searchResultsMenu.add(foundItem.getText()).setAction(foundItem.getAction());
-            }
-            // Put menu right under search field
-            searchResultsMenu.pack();
-            searchResultsMenu.show(mainMenu, searchField.getX(), searchField.getY() + searchField.getHeight());
-
-            // This is tricky. User still is able to edit search text. While Up and Down keys are handled by Popup Menu.
-            searchField.requestFocusInWindow();
-        }
-
-        private void hideMenu() {
-            searchResultsMenu.setVisible(false);
-        }
-    }
 }
diff --git a/src/org/openstreetmap/josm/gui/dialogs/MenuItemSearchDialog.java b/src/org/openstreetmap/josm/gui/dialogs/MenuItemSearchDialog.java
new file mode 100644
index 0000000..2ab0a94
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/dialogs/MenuItemSearchDialog.java
@@ -0,0 +1,127 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
+import org.openstreetmap.josm.tools.Shortcut;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+public class MenuItemSearchDialog extends ExtendedDialog {
+
+    private final Selector selector;
+    private static final MenuItemSearchDialog INSTANCE = new MenuItemSearchDialog(Main.main.menu);
+
+    private MenuItemSearchDialog(MainMenu menu) {
+        super(Main.parent, tr("Search menu items"), new String[]{tr("Select"), tr("Cancel")});
+        this.selector = new Selector(menu);
+        this.selector.setDblClickListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                buttonAction(0, null);
+            }
+        });
+        setContent(selector);
+        setPreferredSize(new Dimension(600, 300));
+    }
+
+    /**
+     * Returns the unique instance of {@code MenuItemSearchDialog}.
+     *
+     * @return the unique instance of {@code MenuItemSearchDialog}.
+     */
+    public static synchronized MenuItemSearchDialog getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public ExtendedDialog showDialog() {
+        selector.init();
+        super.showDialog();
+        selector.clearSelection();
+        return this;
+    }
+
+    @Override
+    protected void buttonAction(int buttonIndex, ActionEvent evt) {
+        super.buttonAction(buttonIndex, evt);
+        if (buttonIndex == 0 && selector.getSelectedItem() != null && selector.getSelectedItem().isEnabled()) {
+            selector.getSelectedItem().getAction().actionPerformed(evt);
+        }
+    }
+
+    private static class Selector extends SearchTextResultListPanel<JMenuItem> {
+
+        private final MainMenu menu;
+
+        public Selector(MainMenu menu) {
+            super();
+            this.menu = menu;
+            lsResult.setCellRenderer(new CellRenderer());
+            lsResult.setSelectionModel(new DefaultListSelectionModel() {
+
+            });
+        }
+
+        public JMenuItem getSelectedItem() {
+            final JMenuItem selected = lsResult.getSelectedValue();
+            if (selected != null) {
+                return selected;
+            } else if (!lsResultModel.isEmpty()) {
+                return lsResultModel.getElementAt(0);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        protected void filterItems() {
+            lsResultModel.setItems(menu.findMenuItems(edSearchText.getText(), true));
+        }
+    }
+
+    private static class CellRenderer implements ListCellRenderer<JMenuItem> {
+
+        private final DefaultListCellRenderer def = new DefaultListCellRenderer();
+
+        @Override
+        public Component getListCellRendererComponent(JList<? extends JMenuItem> list, JMenuItem value, int index, boolean isSelected, boolean cellHasFocus) {
+            final JLabel label = (JLabel) def.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            label.setText(value.getText());
+            label.setIcon(value.getIcon());
+            label.setEnabled(value.isEnabled());
+            final JMenuItem item = new JMenuItem(value.getText());
+            item.setAction(value.getAction());
+            if (isSelected) {
+                item.setBackground(list.getSelectionBackground());
+                item.setForeground(list.getSelectionForeground());
+            } else {
+                item.setBackground(list.getBackground());
+                item.setForeground(list.getForeground());
+            }
+            return item;
+        }
+    }
+
+    public static class Action extends JosmAction {
+
+        public Action() {
+            super(tr("Search menu items"), "dialogs/search", null,
+                    Shortcut.registerShortcut("help:search-items", "Search menu items", KeyEvent.VK_SPACE, Shortcut.CTRL), false);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            MenuItemSearchDialog.getInstance().showDialog();
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java
index 2c5162c..e430097 100644
--- a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java
+++ b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java
@@ -7,13 +7,8 @@ import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -26,7 +21,6 @@ import java.util.Objects;
 import java.util.Set;
 
 import javax.swing.AbstractAction;
-import javax.swing.AbstractListModel;
 import javax.swing.Action;
 import javax.swing.BoxLayout;
 import javax.swing.DefaultListCellRenderer;
@@ -36,10 +30,7 @@ import javax.swing.JLabel;
 import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
-import javax.swing.JScrollPane;
 import javax.swing.ListCellRenderer;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
 
@@ -53,8 +44,8 @@ import org.openstreetmap.josm.gui.tagging.presets.items.Key;
 import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
 import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
 import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
-import org.openstreetmap.josm.gui.widgets.JosmTextField;
 import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
+import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
 import org.openstreetmap.josm.tools.Predicate;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -62,7 +53,7 @@ import org.openstreetmap.josm.tools.Utils;
  * GUI component to select tagging preset: the list with filter and two checkboxes
  * @since 6068
  */
-public class TaggingPresetSelector extends JPanel implements SelectionChangedListener {
+public class TaggingPresetSelector extends SearchTextResultListPanel<TaggingPreset> implements SelectionChangedListener {
 
     private static final int CLASSIFICATION_IN_FAVORITES = 300;
     private static final int CLASSIFICATION_NAME_MATCH = 300;
@@ -72,19 +63,11 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
     private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
     private static final BooleanProperty ONLY_APPLICABLE  = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
 
-    private final JosmTextField edSearchText;
-    private final JList<TaggingPreset> lsResult;
     private final JCheckBox ckOnlyApplicable;
     private final JCheckBox ckSearchInTags;
     private final Set<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class);
     private boolean typesInSelectionDirty = true;
     private final transient PresetClassifications classifications = new PresetClassifications();
-    private final ResultListModel lsResultModel = new ResultListModel();
-
-    private final transient List<ListSelectionListener> listSelectionListeners = new ArrayList<>();
-
-    private transient ActionListener dblClickListener;
-    private transient ActionListener clickListener;
 
     private static class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
         private final DefaultListCellRenderer def = new DefaultListCellRenderer();
@@ -98,30 +81,6 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
         }
     }
 
-    private static class ResultListModel extends AbstractListModel<TaggingPreset> {
-
-        private transient List<PresetClassification> presets = new ArrayList<>();
-
-        public synchronized void setPresets(List<PresetClassification> presets) {
-            this.presets = presets;
-            fireContentsChanged(this, 0, Integer.MAX_VALUE);
-        }
-
-        @Override
-        public synchronized TaggingPreset getElementAt(int index) {
-            return presets.get(index).preset;
-        }
-
-        @Override
-        public synchronized int getSize() {
-            return presets.size();
-        }
-
-        public synchronized boolean isEmpty() {
-            return presets.isEmpty();
-        }
-    }
-
     /**
      * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
      */
@@ -216,68 +175,9 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
      * Constructs a new {@code TaggingPresetSelector}.
      */
     public TaggingPresetSelector(boolean displayOnlyApplicable, boolean displaySearchInTags) {
-        super(new BorderLayout());
-        classifications.loadPresets(TaggingPresets.getTaggingPresets());
-
-        edSearchText = new JosmTextField();
-        edSearchText.getDocument().addDocumentListener(new DocumentListener() {
-            @Override
-            public void removeUpdate(DocumentEvent e) {
-                filterPresets();
-            }
-
-            @Override
-            public void insertUpdate(DocumentEvent e) {
-                filterPresets();
-            }
-
-            @Override
-            public void changedUpdate(DocumentEvent e) {
-                filterPresets();
-            }
-        });
-        edSearchText.addKeyListener(new KeyAdapter() {
-            @Override
-            public void keyPressed(KeyEvent e) {
-                switch (e.getKeyCode()) {
-                case KeyEvent.VK_DOWN:
-                    selectPreset(lsResult.getSelectedIndex() + 1);
-                    break;
-                case KeyEvent.VK_UP:
-                    selectPreset(lsResult.getSelectedIndex() - 1);
-                    break;
-                case KeyEvent.VK_PAGE_DOWN:
-                    selectPreset(lsResult.getSelectedIndex() + 10);
-                    break;
-                case KeyEvent.VK_PAGE_UP:
-                    selectPreset(lsResult.getSelectedIndex() - 10);
-                    break;
-                case KeyEvent.VK_HOME:
-                    selectPreset(0);
-                    break;
-                case KeyEvent.VK_END:
-                    selectPreset(lsResultModel.getSize());
-                    break;
-                }
-            }
-        });
-        add(edSearchText, BorderLayout.NORTH);
-
-        lsResult = new JList<>(lsResultModel);
+        super();
         lsResult.setCellRenderer(new ResultListCellRenderer());
-        lsResult.addMouseListener(new MouseAdapter() {
-            @Override
-            public void mouseClicked(MouseEvent e) {
-                if (e.getClickCount() > 1) {
-                    if (dblClickListener != null)
-                        dblClickListener.actionPerformed(null);
-                } else {
-                    if (clickListener != null)
-                        clickListener.actionPerformed(null);
-                }
-            }
-        });
-        add(new JScrollPane(lsResult), BorderLayout.CENTER);
+        classifications.loadPresets(TaggingPresets.getTaggingPresets());
 
         JPanel pnChecks = new JPanel();
         pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
@@ -289,7 +189,7 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
             ckOnlyApplicable.addItemListener(new ItemListener() {
                 @Override
                 public void itemStateChanged(ItemEvent e) {
-                    filterPresets();
+                    filterItems();
                 }
             });
         } else {
@@ -303,7 +203,7 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
             ckSearchInTags.addItemListener(new ItemListener() {
                 @Override
                 public void itemStateChanged(ItemEvent e) {
-                    filterPresets();
+                    filterItems();
                 }
             });
             pnChecks.add(ckSearchInTags);
@@ -314,7 +214,7 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
         add(pnChecks, BorderLayout.SOUTH);
 
         setPreferredSize(new Dimension(400, 300));
-        filterPresets();
+        filterItems();
         JPopupMenu popupMenu = new JPopupMenu();
         popupMenu.add(new AbstractAction(tr("Add toolbar button")) {
             @Override
@@ -326,21 +226,11 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
         lsResult.addMouseListener(new PopupMenuLauncher(popupMenu));
     }
 
-    private synchronized void selectPreset(int newIndex) {
-        if (newIndex < 0) {
-            newIndex = 0;
-        }
-        if (newIndex > lsResultModel.getSize() - 1) {
-            newIndex = lsResultModel.getSize() - 1;
-        }
-        lsResult.setSelectedIndex(newIndex);
-        lsResult.ensureIndexIsVisible(newIndex);
-    }
-
     /**
      * Search expression can be in form: "group1/group2/name" where names can contain multiple words
      */
-    private synchronized void filterPresets() {
+    @Override
+    protected synchronized void filterItems() {
         //TODO Save favorites to file
         String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
         boolean onlyApplicable = ckOnlyApplicable != null && ckOnlyApplicable.isSelected();
@@ -352,7 +242,12 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
                 text, onlyApplicable, inTags, getTypesInSelection(), selected);
 
         TaggingPreset oldPreset = getSelectedPreset();
-        lsResultModel.setPresets(result);
+        lsResultModel.setItems(Utils.transform(result, new Utils.Function<PresetClassification, TaggingPreset>() {
+            @Override
+            public TaggingPreset apply(PresetClassification x) {
+                return x.preset;
+            }
+        }));
         TaggingPreset newPreset = getSelectedPreset();
         if (!Objects.equals(oldPreset, newPreset)) {
             int[] indices = lsResult.getSelectedIndices();
@@ -486,14 +381,13 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
         typesInSelectionDirty = true;
     }
 
+    @Override
     public synchronized void init() {
         if (ckOnlyApplicable != null) {
             ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
             ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
         }
-        listSelectionListeners.clear();
-        edSearchText.setText("");
-        filterPresets();
+        super.init();
     }
 
     public void init(Collection<TaggingPreset> presets) {
@@ -502,10 +396,6 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
         init();
     }
 
-    public synchronized void clearSelection() {
-        lsResult.getSelectionModel().clearSelection();
-    }
-
     /**
      * Save checkbox values in preferences for future reuse
      */
@@ -542,36 +432,4 @@ public class TaggingPresetSelector extends JPanel implements SelectionChangedLis
     public synchronized void setSelectedPreset(TaggingPreset p) {
         lsResult.setSelectedValue(p, true);
     }
-
-    public synchronized int getItemCount() {
-        return lsResultModel.getSize();
-    }
-
-    public void setDblClickListener(ActionListener dblClickListener) {
-        this.dblClickListener = dblClickListener;
-    }
-
-    public void setClickListener(ActionListener clickListener) {
-        this.clickListener = clickListener;
-    }
-
-    /**
-     * Adds a selection listener to the presets list.
-     * @param selectListener The list selection listener
-     * @since 7412
-     */
-    public synchronized void addSelectionListener(ListSelectionListener selectListener) {
-        lsResult.getSelectionModel().addListSelectionListener(selectListener);
-        listSelectionListeners.add(selectListener);
-    }
-
-    /**
-     * Removes a selection listener from the presets list.
-     * @param selectListener The list selection listener
-     * @since 7412
-     */
-    public synchronized void removeSelectionListener(ListSelectionListener selectListener) {
-        listSelectionListeners.remove(selectListener);
-        lsResult.getSelectionModel().removeListSelectionListener(selectListener);
-    }
 }
diff --git a/src/org/openstreetmap/josm/gui/widgets/SearchTextResultListPanel.java b/src/org/openstreetmap/josm/gui/widgets/SearchTextResultListPanel.java
new file mode 100644
index 0000000..94b5f11
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/widgets/SearchTextResultListPanel.java
@@ -0,0 +1,171 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionListener;
+import java.awt.*;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.*;
+import java.util.List;
+
+public abstract class SearchTextResultListPanel<T> extends JPanel {
+
+    protected final JosmTextField edSearchText;
+    protected final JList<T> lsResult;
+    protected final ResultListModel<T> lsResultModel = new ResultListModel<>();
+
+    protected final transient List<ListSelectionListener> listSelectionListeners = new ArrayList<>();
+
+    private transient ActionListener dblClickListener;
+    private transient ActionListener clickListener;
+
+    protected abstract void filterItems();
+
+    public SearchTextResultListPanel() {
+        super(new BorderLayout());
+
+        edSearchText = new JosmTextField();
+        edSearchText.getDocument().addDocumentListener(new DocumentListener() {
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                filterItems();
+            }
+
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                filterItems();
+            }
+
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+                filterItems();
+            }
+        });
+        edSearchText.addKeyListener(new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                switch (e.getKeyCode()) {
+                    case KeyEvent.VK_DOWN:
+                        selectItem(lsResult.getSelectedIndex() + 1);
+                        break;
+                    case KeyEvent.VK_UP:
+                        selectItem(lsResult.getSelectedIndex() - 1);
+                        break;
+                    case KeyEvent.VK_PAGE_DOWN:
+                        selectItem(lsResult.getSelectedIndex() + 10);
+                        break;
+                    case KeyEvent.VK_PAGE_UP:
+                        selectItem(lsResult.getSelectedIndex() - 10);
+                        break;
+                    case KeyEvent.VK_HOME:
+                        selectItem(0);
+                        break;
+                    case KeyEvent.VK_END:
+                        selectItem(lsResultModel.getSize());
+                        break;
+                }
+            }
+        });
+        add(edSearchText, BorderLayout.NORTH);
+
+        lsResult = new JList<>(lsResultModel);
+        lsResult.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                if (e.getClickCount() > 1) {
+                    if (dblClickListener != null)
+                        dblClickListener.actionPerformed(null);
+                } else {
+                    if (clickListener != null)
+                        clickListener.actionPerformed(null);
+                }
+            }
+        });
+        add(new JScrollPane(lsResult), BorderLayout.CENTER);
+    }
+
+    protected static class ResultListModel<T> extends AbstractListModel<T> {
+
+        private transient List<T> items = new ArrayList<>();
+
+        public synchronized void setItems(List<T> items) {
+            this.items = items;
+            fireContentsChanged(this, 0, Integer.MAX_VALUE);
+        }
+
+        @Override
+        public synchronized T getElementAt(int index) {
+            return items.get(index);
+        }
+
+        @Override
+        public synchronized int getSize() {
+            return items.size();
+        }
+
+        public synchronized boolean isEmpty() {
+            return items.isEmpty();
+        }
+    }
+
+    public synchronized void init() {
+        listSelectionListeners.clear();
+        edSearchText.setText("");
+        filterItems();
+    }
+
+    private synchronized void selectItem(int newIndex) {
+        if (newIndex < 0) {
+            newIndex = 0;
+        }
+        if (newIndex > lsResultModel.getSize() - 1) {
+            newIndex = lsResultModel.getSize() - 1;
+        }
+        lsResult.setSelectedIndex(newIndex);
+        lsResult.ensureIndexIsVisible(newIndex);
+    }
+
+    public synchronized void clearSelection() {
+        lsResult.clearSelection();
+    }
+
+    public synchronized int getItemCount() {
+        return lsResultModel.getSize();
+    }
+
+    public void setDblClickListener(ActionListener dblClickListener) {
+        this.dblClickListener = dblClickListener;
+    }
+
+    public void setClickListener(ActionListener clickListener) {
+        this.clickListener = clickListener;
+    }
+
+    /**
+     * Adds a selection listener to the presets list.
+     *
+     * @param selectListener The list selection listener
+     * @since 7412
+     */
+    public synchronized void addSelectionListener(ListSelectionListener selectListener) {
+        lsResult.getSelectionModel().addListSelectionListener(selectListener);
+        listSelectionListeners.add(selectListener);
+    }
+
+    /**
+     * Removes a selection listener from the presets list.
+     *
+     * @param selectListener The list selection listener
+     * @since 7412
+     */
+    public synchronized void removeSelectionListener(ListSelectionListener selectListener) {
+        listSelectionListeners.remove(selectListener);
+        lsResult.getSelectionModel().removeListSelectionListener(selectListener);
+    }
+}
