### Eclipse Workspace Patch 1.0
#P josm-search
Index: src/org/openstreetmap/josm/data/validation/Test.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/Test.java	(revision 17917)
+++ src/org/openstreetmap/josm/data/validation/Test.java	(working copy)
@@ -274,6 +274,7 @@
     public boolean ok() {
         enabled = checkEnabled.isSelected();
         testBeforeUpload = checkBeforeUpload.isSelected();
+        checkBeforeUpload = null; //prevent memory leak (keeps reference to parent dialog)
         return false;
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/PreferenceDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/PreferenceDialog.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/PreferenceDialog.java	(working copy)
@@ -41,7 +41,7 @@
  */
 public class PreferenceDialog extends JDialog {
 
-    private final PreferenceTabbedPane tpPreferences = new PreferenceTabbedPane();
+    private PreferenceTabbedPane tpPreferences = new PreferenceTabbedPane();
     private final ContextSensitiveHelpAction helpAction = new ContextSensitiveHelpAction();
     private final WindowEventHandler windowEventHandler = new WindowEventHandler();
     private boolean canceled;
@@ -154,7 +154,7 @@
         } else if (previouslySelected != null && previouslySelected.a != null) {
             tpPreferences.selectTabByPref(previouslySelected.a);
         } else {
-            tpPreferences.setSelectedIndex(0);
+            tpPreferences.setSelectedIndex(1);
         }
     }
 
@@ -232,6 +232,8 @@
         previouslySelected = tpPreferences.getSelectedTab();
         removeWindowListener(windowEventHandler);
         setVisible(false); // save current geometry
+        //removeAll();
+        tpPreferences.searchPanel.destroyIndex();
         super.dispose();
     }
 }
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java	(working copy)
@@ -0,0 +1,290 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.swing.ImageIcon;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Contains searchable items and their components
+ * @author Bjoeni
+ */
+public class SearchItem {
+    int level;
+    int inset;
+    private String text;
+    private String tooltip;
+    private String highlight;
+    private List<SearchItem> children = new ArrayList<>();
+    private List<Component> originalComponents = new ArrayList<>();
+
+    private int tabIndex;
+    private boolean eol = true;
+    private boolean visible = true;
+    private boolean hasVisibleChildren;
+    private boolean childComponentsSearchable = true;
+    private String iconName;
+    private String textNormalized;
+    private String tooltipNormalized;
+    private PreferenceTabbedPane tabs;
+    private SearchItemComponent comp;
+
+    public SearchItem(String text) {
+        this.setText(text);
+    }
+
+    public SearchItem(Component originalComponent, String text, String tooltip, boolean eol,
+            boolean childComponentsSearchable) {
+        this.originalComponents.add(originalComponent);
+        this.setText(text);
+        this.setTooltip(tooltip);
+        this.setEOL(eol);
+        this.childComponentsSearchable = childComponentsSearchable;
+    }
+
+    public SearchItem addChild(SearchItem item) {
+        synchronized (children) {
+            Optional<SearchItem> match = children.stream()
+                    .filter(c -> Objects.equals(c.text, item.text) && c.level == item.level && c.inset == item.inset)
+                    .findAny();
+            if (match.isPresent()) {
+                match.get().merge(item);
+                return match.get();
+            } else {
+                children.add(item);
+                GuiHelper.runInEDT(() -> {
+                    getComponent().addChild(item.getComponent());
+                });
+                return item;
+            }
+        }
+
+    }
+
+    public void merge(SearchItem item) {
+        this.originalComponents.addAll(item.originalComponents);
+        this.setEOL(this.isEOL() || item.isEOL());
+    }
+
+    public boolean filter(SearchFilters filters) {
+        visible = this.matches(filters);
+        hasVisibleChildren = false;
+        for (SearchItem child : children) {
+            if (child.isVisible()) {
+                hasVisibleChildren = child.filter(filters) || hasVisibleChildren;
+            }
+        }
+        visible = visible || hasVisibleChildren;
+
+        if (visible && text != null && textNormalized != null) {
+            highlight = filters.highlightString(text, textNormalized);
+        } else {
+            highlight = "<html><span style=\"color: red;\"><b>ERROR</b></span></html>";
+        }
+        return visible;
+    }
+
+    public boolean matches(SearchFilters filters) {
+        return filters.getNormalized().stream()
+                .allMatch(str -> textNormalized != null && textNormalized.indexOf(str) != -1
+                        || (tooltipNormalized != null && tooltipNormalized.indexOf(str) != -1));
+    }
+
+    public void showAll() {
+        visible = true;
+        children.forEach(SearchItem::showAll);
+    }
+
+    @Override
+    public String toString() {
+        return text + (tooltip == null ? "" : " (Tooltip: " + tooltip + ")") + " [" + level + "." + inset
+                + (eol ? ":EOL" : "") + "] {" + originalComponents.size() + "}";
+    }
+
+    public ImageIcon getIcon() {
+        if (iconName == null)
+            return null;
+
+        return iconName == null || iconName.isEmpty() ? null
+                : iconName.contains("/") ? ImageProvider.get(iconName, ImageSizes.SMALLICON)
+                        : ImageProvider.get("preferences", iconName, ImageSizes.SMALLICON);
+    }
+
+    public boolean isVisible() {
+        return visible;
+    }
+
+    public boolean isEOL() {
+        return eol;
+    }
+
+    public void setEOL() {
+        setEOL(true);
+    }
+
+    public void setEOL(boolean eol) {
+        this.eol = eol;
+    }
+
+    public void setLevelInset(int level, int inset) {
+        this.level = level;
+        this.inset = inset;
+    }
+
+    public void maximizeInset() {
+        this.inset = Integer.MAX_VALUE;
+    }
+
+    public void setTabIndex(PreferenceTabbedPane tabs, int tabIndex) {
+        this.tabs = tabs;
+        this.tabIndex = tabIndex;
+    }
+
+    public void setIconName(String iconName) {
+        this.iconName = iconName;
+    }
+
+    public void addOriginalComponent(Component c) {
+        this.originalComponents.add(c);
+    }
+
+    public void showOriginalComponent() {
+        if (tabIndex > -1) {
+            tabs.setSelectedIndex(tabIndex);
+        }
+        originalComponents.stream().filter(Objects::nonNull).forEach(comp -> {
+            //Color bg = comp.getBackground();
+            Color fg = comp.getForeground();
+            //Logging.debug("BG:" + bg.toString());
+            Logging.debug("FG:" + fg.toString());
+            //comp.setBackground(SystemColor.text);
+            comp.setForeground(SearchPanel.DARK_MODE ? Color.yellow : Color.red);
+            comp.requestFocus();
+            if (comp instanceof JComponent) {
+                JComponent jcomp = (JComponent) comp;
+                Rectangle bounds = new Rectangle(jcomp.getBounds());
+                jcomp.scrollRectToVisible(bounds);
+            }
+            Timer timer = new Timer(3000, l -> {
+                //comp.setBackground(bg);
+                comp.setForeground(fg);
+            });
+            timer.setRepeats(false);
+            timer.start();
+        });
+        if (originalComponents.stream().filter(Objects::nonNull).noneMatch(Component::isVisible)) {
+            Logging.warn("INVIS!!");
+        }
+    }
+
+    /**
+     * @return the component
+     */
+    public SearchItemComponent getComponent() {
+        if (comp == null) {
+            comp = GuiHelper.runInEDTAndWaitAndReturn(() -> new SearchItemComponent(this));
+        }
+        return comp;
+    }
+
+    public void updateComponents() throws InvocationTargetException, InterruptedException {
+        SwingUtilities.invokeAndWait(() -> {
+            //invokeLater is faster but causes too many calls at once and blocks the UI
+            SearchItemComponent c = getComponent();
+            c.setVisible(this.isVisible());
+            if (isVisible()) {
+                c.setToolTipText(toString());
+                c.setText(highlight);
+                c.setExpanded(true);
+            }
+        });
+        if (isVisible()) {
+            for (SearchItem child : children) {
+                child.updateComponents();
+            }
+        }
+    }
+
+    /**
+     * @return the hasVisibleChildren
+     */
+    public boolean hasVisibleChildren() {
+        return hasVisibleChildren;
+    }
+
+    /**
+     * @return the childComponentsSearchable
+     */
+    public boolean isChildComponentsSearchable() {
+        return childComponentsSearchable;
+    }
+
+    /**
+     * @return the text2
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * @param text the text to set
+     */
+    public void setText(String text) {
+        this.text = stripHtml(text);
+        this.textNormalized = SearchFilters.normalize(this.text);
+    }
+
+    /**
+     * @return the tooltip
+     */
+    public String getTooltip() {
+        return tooltip;
+    }
+
+    public void addTooltip(String tooltip) {
+        if (this.getTooltip() == null || this.getTooltip().trim().isEmpty()) {
+            this.setTooltip(tooltip);
+        }
+    }
+
+    /**
+     * @param tooltip the tooltip to set
+     */
+    public void setTooltip(String tooltip) {
+        this.tooltip = stripHtml(tooltip);
+        this.tooltipNormalized = SearchFilters.normalize(this.tooltip);
+    }
+
+    private static String stripHtml(String str) {
+        if (str == null || str.trim().length() == 0)
+            return null;
+
+        if (str.indexOf('<') > -1) {
+            str = str.replaceAll("(?i)(<br\\s/>\\s*)+", ": ").replaceAll("(<style>.*</style>|<[^<>]*>)", " ").trim()
+                    .replaceAll(" +", " ");
+        }
+
+        if (str.indexOf(':') == str.length() - 1) {
+            str = str.substring(0, str.length() - 1);
+        }
+
+        return str;
+    }
+
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/shortcut/PrefJPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/shortcut/PrefJPanel.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/shortcut/PrefJPanel.java	(working copy)
@@ -13,6 +13,7 @@
 import java.awt.event.KeyEvent;
 import java.awt.im.InputContext;
 import java.lang.reflect.Field;
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -36,6 +37,8 @@
 import javax.swing.table.TableColumnModel;
 
 import org.openstreetmap.josm.data.preferences.NamedColorProperty;
+import org.openstreetmap.josm.gui.preferences.search.SearchItem;
+import org.openstreetmap.josm.gui.preferences.search.ISearchableComponent;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.TableHelper;
 import org.openstreetmap.josm.gui.widgets.FilterField;
@@ -48,7 +51,7 @@
 /**
  * This is the keyboard preferences content.
  */
-public class PrefJPanel extends JPanel {
+public class PrefJPanel extends JPanel implements ISearchableComponent {
 
     // table of shortcuts
     private final AbstractTableModel model;
@@ -350,4 +353,18 @@
             }
         }
     }
+
+    @Override
+    public boolean isChildrenSearchable() {
+        return false;
+    }
+
+    @Override
+    public List<SearchItem> getSearchItems() {
+        List<SearchItem> list = new ArrayList<>();
+        for (int row = 0; row < model.getRowCount(); row++) {
+            list.add(new SearchItem(model.getValueAt(row, 0).toString()));
+        }
+        return list;
+    }
 }
Index: src/org/openstreetmap/josm/gui/preferences/SourceEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/SourceEditor.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/SourceEditor.java	(working copy)
@@ -83,6 +83,8 @@
 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.preferences.search.ISearchableComponent;
+import org.openstreetmap.josm.gui.preferences.search.SearchItem;
 import org.openstreetmap.josm.gui.util.DocumentAdapter;
 import org.openstreetmap.josm.gui.util.FileFilterAllFiles;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -110,7 +112,7 @@
  * Editor for JOSM extensions source entries.
  * @since 1743
  */
-public abstract class SourceEditor extends JPanel {
+public abstract class SourceEditor extends JPanel implements ISearchableComponent {
 
     /** the type of source entry **/
     protected final SourceType sourceType;
@@ -563,6 +565,15 @@
         sourcesInitiallyLoaded = true;
     }
 
+    @Override
+    public List<SearchItem> getSearchItemsAsync() {
+        if (!sourcesInitiallyLoaded && !NetworkManager.isOffline(OnlineResource.CACHE_UPDATES)) {
+            new SourceLoader(availableSourcesUrl, sourceProviders, true).run();
+        }
+        sourcesInitiallyLoaded = true;
+        return null;
+    }
+
     /**
      * List model of available sources.
      */
@@ -1349,11 +1360,17 @@
         private CachedFile cachedFile;
         private boolean canceled;
         private final List<ExtendedSourceEntry> sources = new ArrayList<>();
+        private boolean silent;
 
         SourceLoader(String url, List<SourceProvider> sourceProviders) {
-            super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url));
+            this(url, sourceProviders, false);
+        }
+
+        SourceLoader(String url, List<SourceProvider> sourceProviders, boolean silent) {
+            super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url), true, silent);
             this.url = url;
             this.sourceProviders = sourceProviders;
+            this.silent = silent;
         }
 
         @Override
@@ -1363,16 +1380,17 @@
         }
 
         protected void warn(Exception e) {
-            String emsg = Utils.escapeReservedCharactersHTML(e.getMessage() != null ? e.getMessage() : e.toString());
-            final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg);
+            if (silent) {
+                Logging.warn(e);
+            } else {
+                String emsg = Utils
+                        .escapeReservedCharactersHTML(e.getMessage() != null ? e.getMessage() : e.toString());
+                final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg);
 
-            GuiHelper.runInEDT(() -> HelpAwareOptionPane.showOptionDialog(
-                    MainApplication.getMainFrame(),
-                    msg,
-                    tr("Error"),
-                    JOptionPane.ERROR_MESSAGE,
-                    ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC))
-                    ));
+                GuiHelper.runInEDT(() -> HelpAwareOptionPane.showOptionDialog(MainApplication.getMainFrame(), msg,
+                        tr("Error"), JOptionPane.ERROR_MESSAGE,
+                        ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC))));
+            }
         }
 
         @Override
Index: src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java	(working copy)
@@ -0,0 +1,60 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.util.List;
+
+/**
+ * Interface allowing components in the preferences to determine if and how they can be searched
+ * @author Bjoeni
+ */
+public interface ISearchableComponent {
+    /**
+     * @return whether the component can be searched<br><br>
+     *
+     * default: true
+     */
+    default boolean isSearchable() {
+        return true;
+    }
+
+    /**
+     * @return whether the children of this component should be traversed <br><br>
+     *
+     * default: same as {@link #isSearchable()} unless explicitly overridden
+     */
+    default boolean isChildrenSearchable() {
+        return isSearchable();
+    }
+
+    /**
+     * Called from the EventDispatchThread (GUI), should not be blocked.
+     *
+     * @return the {@link SearchItem}s that should be used. Ignored if {@link #isSearchable()} returns {@code false}.<br>
+     * Overrides the default {@link SearchTextFinder} if returning not {@code null} (including if an empty list is returned)<br><br>
+     *
+     * default: null
+     */
+    default List<SearchItem> getSearchItems() {
+        return null;
+    }
+
+    /**
+     * Called from the worker thread, can be blocked as long as necessary. Calls to the GUI must be placed in the EventDispatchThread.
+     *
+     * @return the {@link SearchItem}s that should be used. Ignored if {@link #isSearchable()} returns {@code false}.<br>
+     * Overrides the default {@link SearchTextFinder} if returning not {@code null} (including if an empty list is returned)<br><br>
+     *
+     * default: null
+     */
+    default List<SearchItem> getSearchItemsAsync() {
+        return null;
+    }
+
+    /**
+     * @return
+     */
+    default String getIconName() {
+        return null;
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/DefaultTabPreferenceSetting.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/DefaultTabPreferenceSetting.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/DefaultTabPreferenceSetting.java	(working copy)
@@ -48,7 +48,7 @@
         this.tabpane = tabpane;
         this.subSettingMap = tabpane != null ? new HashMap<>() : null;
         if (tabpane != null) {
-            tabpane.addMouseWheelListener(new PreferenceTabbedPane.WheelListener(tabpane));
+            tabpane.addMouseWheelListener(new PreferenceTabbedPane.MouseAndWheelListener(tabpane));
         }
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java	(working copy)
@@ -0,0 +1,169 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.SystemColor;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Panel displaying search results in the preferences
+ * @author Bjoeni
+ */
+public class SearchPanel extends JScrollPane implements ISearchableComponent {
+
+    public static final boolean DARK_MODE = 130 < (Math.sqrt(Math.pow(SystemColor.text.getRed(), 2) * .241
+            + Math.pow(SystemColor.text.getGreen(), 2) * .691 + Math.pow(SystemColor.text.getBlue(), 2) * .068));
+
+    private PreferenceTabbedPane tabs;
+    private SearchIndex searchIndex;
+    private JLabel lblStatus = new JLabel(tr("Enter something in the text fied to start searching"));
+    private JPanel panel = new JPanel(new GridBagLayout());
+    private SearchThread thread;
+
+    public SearchPanel(PreferenceTabbedPane tabs) {
+        super(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        this.tabs = tabs;
+        searchIndex = new SearchIndex(tabs);
+        GBC gbc = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc.anchor = GBC.NORTHWEST;
+        gbc.weightx = 1;
+        panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+        panel.add(searchIndex.getPanel(), gbc);
+        gbc.gridy = 1;
+        lblStatus.setHorizontalAlignment(SwingConstants.CENTER);
+        gbc.fill();
+        gbc.anchor = GBC.CENTER;
+        panel.add(lblStatus, gbc);
+        //setAlignmentX(LEFT_ALIGNMENT);
+        setViewportView(panel);
+    }
+
+    public void search(String text) {
+        if (thread != null && thread.isAlive()) {
+            Logging.debug("alive");
+            thread.awaitBuildAndRestart(text);
+        } else {
+            Logging.debug("dead");
+            thread = new SearchThread(text);
+            thread.start();
+        }
+    }
+
+    public PreferenceTabbedPane getTabPane() {
+        return tabs;
+    }
+
+    public void destroyIndex() {
+        removeAll();
+        panel = null;
+        lblStatus = null;
+        setViewport(null);
+        this.searchIndex = null;
+    }
+
+    @Override
+    public boolean isSearchable() {
+        return false;
+    }
+
+    class SearchThread extends Thread {
+        private String text;
+        private StringBuilder searchableSettings;
+        private boolean building;
+        private boolean interrupted;
+        private String nextText;
+        private boolean justBuiltIndex;
+
+        public SearchThread(String text) {
+            this.text = text;
+        }
+
+        public SearchThread(String text, boolean justBuiltIndex) {
+            this.text = text;
+            this.justBuiltIndex = justBuiltIndex;
+        }
+
+        @Override
+        public void run() {
+            try {
+                boolean buildIndex = !searchIndex.isBuilt(); // || "".equals(text);
+                if (buildIndex) {
+                    building = true;
+                    Logging.info("Building search index...");
+                    setText(tr("Building search index..."));
+                    searchIndex.build();
+                    building = false;
+                    if (interrupted) {
+                        startNext(true);
+                        return;
+                    }
+                }
+
+                if (buildIndex || justBuiltIndex) {
+                    setText(tr("Searching..."));
+                }
+
+                //if (!"".equals(text)) {
+                Logging.debug("searching \"" + text + "\"");
+                boolean found = searchIndex.filter(text);
+                Logging.debug("searched \"" + text + "\"");
+                //}
+
+                Logging.debug("before \"" + text + "\"");
+                searchIndex.updateComponents();
+                Logging.debug("done \"" + text + "\"");
+
+                String txt = "";
+                if (!found) {
+                    txt += tr("No results found.") + "<br><br>";
+                }
+                if (false) { //expertmode
+                    txt += tr("You might get more results by enabling expert mode.");
+                }
+                setText(txt);
+
+            } catch (InterruptedException e) {
+                Logging.debug("Interrupted search thread \"" + text + "\"");
+                // to be expected
+            } catch (InvocationTargetException e) {
+                throw new JosmRuntimeException(e);
+            }
+        }
+
+        public void awaitBuildAndRestart(String nextText) {
+            this.nextText = nextText;
+            if (building) {
+                interrupted = true;
+            } else {
+                interrupt();
+                startNext(false);
+            }
+        }
+
+        private void startNext(boolean justBuiltIndex) {
+            thread = new SearchThread(nextText, justBuiltIndex);
+            thread.start();
+        }
+
+        private void setText(String txt) {
+            SwingUtilities.invokeLater(() -> {
+                lblStatus.setText(txt);
+            });
+        }
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java	(working copy)
@@ -0,0 +1,304 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.LayoutManager;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.stream.Collectors;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferenceTab;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.LanguageInfo;
+
+/**
+ * Contains all {@link SearchItem}s
+ * @author Bjoeni
+ */
+public class SearchIndex {
+    public static final Locale CURRENT_LOCALE = LanguageInfo.getLocale(LanguageInfo.getJOSMLocaleCode());
+    public static final GBC EOL = GBC.eol();
+
+    Stack<SearchItem> stack = new Stack<>();
+    List<SearchItem> parents = new ArrayList<>();
+    PreferenceTabbedPane prefTabs;
+    GridBagConstraints lastConstraint;
+    private boolean built;
+    private String lastFilterStr;
+    private int currentTabIndex = -1;
+
+    private JPanel panel = GuiHelper.runInEDTAndWaitAndReturn(() -> new JPanel(new GridBagLayout()));
+
+    GridBagConstraints gbc_nw;
+
+    public SearchIndex(PreferenceTabbedPane prefTabs) {
+        this.prefTabs = prefTabs;
+        gbc_nw = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc_nw.anchor = GBC.NORTHWEST;
+    }
+
+    void add(SearchItem item) {
+        if (item.getText() == null)
+            return;
+
+        item.setTabIndex(prefTabs, currentTabIndex);
+        if (stack.isEmpty()) {
+            item.setLevelInset(0, 0);
+            parents.add(item);
+            GuiHelper.runInEDT(() -> {
+                getPanel().add(item.getComponent(), gbc_nw);
+            });
+            stack.push(item);
+        } else {
+            SearchItem lastItem = stack.lastElement();
+            if (!lastItem.isEOL() && lastItem.level == item.level) {
+                lastItem.setEOL(item.isEOL());
+                lastItem.addChild(item);
+            } else if (lastItem.level < item.level
+                    || (lastItem.level == item.level && lastItem.inset + 15 < item.inset)) {
+                stack.push(lastItem.addChild(item));
+            } else {
+                stack.pop();
+                add(item);
+            }
+        }
+    }
+
+    void addAll(List<SearchItem> items, boolean isEOL) {
+        if (items.size() > 0) {
+            items.get(items.size() - 1).setEOL(isEOL);
+        }
+        items.forEach(item -> add(item));
+    }
+
+    void insertEOL() {
+        if (!stack.isEmpty()) {
+            stack.lastElement().setEOL();
+        }
+    }
+
+    void insertSeparator() {
+        if (!stack.isEmpty()) {
+            stack.lastElement().maximizeInset();
+        }
+    }
+
+    void insertNewPage() {
+        stack.clear();
+    }
+
+    public boolean isBuilt() {
+        return built;
+    }
+
+    /**
+     * @throws InvocationTargetException exception while initializing tab
+     * @throws InterruptedException thread interrupted
+     */
+    public void build() throws InvocationTargetException, InterruptedException {
+        //setVisible(false);
+        built = false;
+        parents.clear();
+        getPanel().removeAll();
+        for (int i = 0; i < prefTabs.getTabCount(); i++) {
+            Component c = prefTabs.getComponentAt(i);
+            currentTabIndex = i;
+            String iconName = null;
+            if (c instanceof PreferenceTab) {
+                iconName = ((PreferenceTab) c).getTabPreferenceSetting().getIconName();
+                final int index = i;
+                final Component comp = c;
+                SwingUtilities.invokeAndWait(() -> prefTabs.initializeTab(index, (PreferenceTab) comp, true));
+                c = prefTabs.getComponentAt(i);
+            }
+            insertNewPage();
+            searchComponent(c, 1, iconName);
+        }
+
+        built = true;
+        //setVisible(true);
+    }
+
+    public boolean filter(String filterStr) {
+        boolean found = false;
+
+        if (lastFilterStr != null && filterStr.indexOf(lastFilterStr) != 0) {
+            parents.forEach(SearchItem::showAll);
+        }
+
+        if (!filterStr.isEmpty()) {
+            SearchFilters filters = new SearchFilters(filterStr);
+            found = parents.stream()
+                    .filter(SearchItem::isVisible)
+                    .map(item -> item.filter(filters))
+                    .collect(Collectors.toList()) //force all visible elements to be evaluated
+                    .contains(true);
+        }
+
+        lastFilterStr = filterStr;
+        return found;
+    }
+
+    /*public DefaultMutableTreeNode getTreeView() {
+        return tree;
+    }
+
+    public synchronized DefaultMutableTreeNode createTreeView(DefaultTreeModel model) {
+        tree.removeAllChildren();
+        parents.forEach(child -> {
+            MutableTreeNode t = child.createTreeView(model);
+            if (t != null) {
+                model.insertNodeInto(t, tree, tree.getChildCount());
+                //tree.add(t);
+            }
+        });
+        return tree;
+    }*/
+
+    private boolean searchComponent(Component comp, int level, String iconName)
+            throws InvocationTargetException, InterruptedException {
+
+        final Component c = comp;
+
+        boolean isEOL = true;
+        GridBagConstraints currentConstraint = null;
+
+        Container p = c.getParent();
+        if (p != null) {
+            LayoutManager layout = p.getLayout();
+            if (layout != null && layout instanceof GridBagLayout) {
+                GridBagLayout grid = (GridBagLayout) layout;
+
+                currentConstraint = grid.getConstraints(c);
+                isEOL = currentConstraint.gridwidth == GridBagConstraints.REMAINDER;
+            }
+        }
+
+        if (lastConstraint != null && currentConstraint != null
+                && (((lastConstraint.fill == GridBagConstraints.HORIZONTAL
+                        || lastConstraint.fill == GridBagConstraints.BOTH)
+                        && currentConstraint.fill != GridBagConstraints.HORIZONTAL
+                        && currentConstraint.fill != GridBagConstraints.BOTH)
+                        || lastConstraint.gridy != currentConstraint.gridy)) {
+            insertEOL();
+        }
+
+        lastConstraint = currentConstraint;
+        boolean isChildrenSearchable = true;
+        ISearchableComponent s = null;
+        if (c instanceof ISearchableComponent) {
+            s = (ISearchableComponent) c;
+            isChildrenSearchable = s.isChildrenSearchable();
+        }
+
+        if (s == null || s.isSearchable()) {
+
+            List<SearchItem> items = null;
+            if (s != null) {
+                items = s.getSearchItemsAsync();
+                final ISearchableComponent s2 = s;
+                List<SearchItem> itm2 = GuiHelper.runInEDTAndWaitAndReturn(() -> s2.getSearchItems());
+                if (itm2 != null) {
+                    if (items == null) {
+                        items = itm2;
+                    } else {
+                        items.addAll(itm2);
+                    }
+                }
+
+                if (iconName == null) {
+                    iconName = s.getIconName();
+                }
+
+                /*final SwingTransferItem swing = new SwingTransferItem(s);
+                SwingUtilities.invokeAndWait(() -> {
+                    swing.list = swing.component.getSearchItems();
+                });
+
+                if (swing.list != null) {
+                    items = swing.list;
+                }*/
+            }
+            if (items == null) {
+                final List<SearchItem> itm = new ArrayList<>();
+                SwingUtilities.invokeAndWait(() -> {
+                    SearchTextFinder.DEFAULT_SEARCH_TEXT_FINDERS
+                            .forEach(finder -> itm.addAll(finder.getSearchItems(c)));
+                });
+
+                items = itm;
+            }
+            if (iconName != null) {
+                final String iconName2 = iconName;
+                items.forEach(item -> item.setIconName(iconName2));
+            }
+            if (items.size() == 0) {
+                if (isEOL) {
+                    insertEOL();
+                }
+                if (c instanceof JSeparator) {
+                    insertSeparator();
+                }
+
+            } else {
+                items.get(0).addOriginalComponent(c);
+                if (c instanceof JComponent) {
+                    items.get(0).setTooltip(((JComponent) c).getToolTipText());
+                }
+
+                final int inset = currentConstraint == null ? 0 : currentConstraint.insets.left;
+                items.forEach(item -> item.setLevelInset(level, inset));
+                isChildrenSearchable = isChildrenSearchable
+                        && items.stream().allMatch(item -> item.isChildComponentsSearchable());
+
+                addAll(items, isEOL);
+            }
+        }
+
+        if (isChildrenSearchable && c instanceof Container) {
+            Container cont = (Container) c;
+            List<Component> components = Arrays.asList(cont.getComponents());
+            if (components.size() > 0) {
+                insertEOL();
+                for (Component component : components) {
+                    searchComponent(component, level + 1, iconName);
+                }
+            }
+        }
+        return isEOL;
+    }
+
+    /**
+     * @return the panel
+     */
+    public JPanel getPanel() {
+        return panel;
+    }
+
+    public void setVisible(boolean visible) {
+        GuiHelper.runInEDT(() -> {
+            panel.setVisible(visible);
+        });
+    }
+
+    public void updateComponents() throws InvocationTargetException, InterruptedException {
+        for (SearchItem item : parents) {
+            item.updateComponents();
+        }
+    }
+
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java	(working copy)
@@ -1,6 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.preferences.plugin;
 
+import static java.awt.GridBagConstraints.BOTH;
 import static java.awt.GridBagConstraints.HORIZONTAL;
 import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trc;
@@ -51,6 +52,8 @@
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
+import org.openstreetmap.josm.gui.preferences.search.NotSearchablePanel;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.widgets.FilterField;
 import org.openstreetmap.josm.plugins.PluginDownloadTask;
@@ -162,7 +165,7 @@
     }
 
     private JPanel buildSearchFieldPanel() {
-        JPanel pnl = new JPanel(new GridBagLayout());
+        JPanel pnl = new NotSearchablePanel(new GridBagLayout());
         pnl.add(GBC.glue(0, 0));
 
         ButtonGroup bg = new ButtonGroup();
@@ -231,12 +234,18 @@
 
     @Override
     public void addGui(final PreferenceTabbedPane gui) {
+        PreferencePanel tab = gui.createPreferenceTab(this);
         JTabbedPane pane = getTabPane();
+        JPanel pnlPluginList = buildPluginListPanel();
         pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel();
-        pane.addTab(tr("Plugins"), buildPluginListPanel());
+        pane.addTab(tr("Plugins"), pnlPluginList);
         pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy);
-        super.addGui(gui);
-        readLocalPluginInformation();
+        tab.add(pane, GBC.eol().fill(BOTH));
+        gui.addChangeListener(e -> {
+            if (gui.getSelectedComponent() == tab) {
+                readLocalPluginInformation();
+            }
+        });
         pluginPreferencesActivated = true;
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/search/package-info.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/package-info.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/package-info.java	(working copy)
@@ -0,0 +1 @@
+package org.openstreetmap.josm.gui.preferences.search;
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java	(working copy)
@@ -0,0 +1,151 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagLayout;
+import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+
+public class SearchItemComponent extends JPanel {
+
+    private final GridBagLayout own_gbl = new GridBagLayout();
+    private final JLabel lbl = new JLabel();
+    private final JLabel expander = new JLabel();
+    private JPanel childPanel;
+    private final SearchItem item;
+
+    private GridBagLayout parent_gbl;
+    private boolean expanded = true;
+    private GBC gbc_nw;
+    private GBC gbc_nw_fill;
+
+    private static final ImageIcon ICON_EXPANDED = ImageProvider.get("dialogs", "down", ImageSizes.SMALLICON);
+    private static final ImageIcon ICON_COLLAPSED = ImageProvider.get("dialogs", "next", ImageSizes.SMALLICON);
+
+    private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
+    private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
+
+    public SearchItemComponent(SearchItem item) {
+        super(new GridBagLayout());
+        setVisible(false);
+        this.item = item;
+        add(expander);
+
+        gbc_nw = GBC.eol();
+        gbc_nw.anchor = GBC.NORTHWEST;
+        gbc_nw_fill = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc_nw_fill.anchor = GBC.NORTHWEST;
+        add(lbl, gbc_nw_fill);
+        lbl.setCursor(CURSOR_HAND);
+        lbl.setBackground(Color.cyan);
+        //lbl.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0));
+        setBorder(BorderFactory.createEmptyBorder(10, item.level == 0 ? 10 : 17, item.level == 0 ? 25 : 0, 10));
+
+        expander.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                setExpanded(!isExpanded());
+            }
+        });
+
+        addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                item.showOriginalComponent();
+            }
+        });
+    }
+
+    public void setText(String txt) {
+        lbl.setText(txt);
+    }
+
+    @Override
+    protected void paintComponent(Graphics graphics) {
+        super.paintComponent(graphics);
+
+        if (item.level != 0)
+            return;
+
+        Graphics2D g = (Graphics2D) graphics;
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+        int width = getWidth(),
+                height = getHeight(),
+                stroke = 1,
+                offset = 5,
+                radius = 15;
+
+
+        g.setColor(new Color(0, 0, 0, 125));
+        g.fillRoundRect(offset, offset, width - stroke - offset, height - stroke - offset - 10, radius, radius);
+
+        g.setColor(getBackground());
+        g.fillRoundRect(0, 0, width - 5, height - 15, radius, radius);
+
+        g.setColor(getForeground());
+        g.setStroke(new BasicStroke(stroke));
+        g.drawRoundRect(0, 0, width - 5, height - 15, radius, radius);
+
+        g.setStroke(new BasicStroke());
+    }
+
+    /**
+     * @return the expanded
+     */
+    public boolean isExpanded() {
+        return expanded;
+    }
+
+    /**
+     * @param expanded the expanded to set
+     */
+    public void setExpanded(boolean expanded) {
+        if (item.hasVisibleChildren()) {
+            this.expanded = expanded;
+            expander.setIcon(expanded ? ICON_EXPANDED : ICON_COLLAPSED);
+            expander.setCursor(CURSOR_HAND);
+            getChildPanel().setVisible(expanded);
+        } else {
+            expander.setIcon(ICON_COLLAPSED);
+        }
+    }
+
+    /**
+     * @return the childPanel
+     */
+    public JPanel getChildPanel() {
+        if (childPanel == null) {
+            childPanel = new JPanel(own_gbl);
+            add(childPanel, gbc_nw_fill);
+        }
+        return childPanel;
+    }
+
+    public void addChild(SearchItemComponent comp) {
+        getChildPanel().add(comp, gbc_nw_fill);
+        comp.setParentGBL(own_gbl);
+    }
+
+    /**
+     * @param parent_gbl the parent_gbl to set
+     */
+    public void setParentGBL(GridBagLayout parent_gbl) {
+        this.parent_gbl = parent_gbl;
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java
===================================================================
--- src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java	(working copy)
@@ -18,7 +18,7 @@
 import org.xml.sax.SAXException;
 
 /**
- * Instanced of this thread will display a "Please Wait" message in middle of JOSM
+ * Instanced of this thread can display a "Please Wait" message in middle of JOSM
  * to indicate a progress being executed.
  *
  * @author Imi
@@ -30,6 +30,12 @@
     /** progress monitor */
     protected final ProgressMonitor progressMonitor;
 
+    protected PleaseWaitRunnable(String title, boolean ignoreException, boolean silent) {
+        this.title = title;
+        this.ignoreException = ignoreException;
+        this.progressMonitor = new PleaseWaitProgressMonitor(title, silent);
+    }
+
     /**
      * Create the runnable object with a given message for the user.
      * @param title message for the user
Index: src/org/openstreetmap/josm/gui/preferences/plugin/PluginListPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/plugin/PluginListPanel.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/plugin/PluginListPanel.java	(working copy)
@@ -9,9 +9,13 @@
 import java.awt.Rectangle;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.swing.JComponent;
 import javax.swing.JLabel;
@@ -18,14 +22,21 @@
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.gui.preferences.search.ISearchableComponent;
+import org.openstreetmap.josm.gui.preferences.search.SearchItem;
+import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
+import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
 import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
+import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
 
 /**
  * A panel displaying the list of known plugins.
  */
-public class PluginListPanel extends VerticallyScrollablePanel {
+public class PluginListPanel extends VerticallyScrollablePanel implements ISearchableComponent {
     static final class PluginCheckBoxMouseAdapter extends MouseAdapter {
         private final PluginCheckBox cbPlugin;
 
@@ -118,9 +129,12 @@
     /**
      * Displays a list of plugins.
      * @param displayedPlugins list of plugins
+     * @return the added labels
      * @since 13799
      */
-    public void displayPluginList(List<PluginInformation> displayedPlugins) {
+    public Map<PluginInformation, JLabel> displayPluginList(List<PluginInformation> displayedPlugins) {
+        Map<PluginInformation, JLabel> lbls = new HashMap<>();
+
         GridBagConstraints gbc = new GridBagConstraints();
         gbc.gridx = 0;
         gbc.anchor = GridBagConstraints.NORTHWEST;
@@ -143,6 +157,7 @@
                     pi.getScaledIcon(),
                     SwingConstants.LEADING);
             lblPlugin.addMouseListener(new PluginCheckBoxMouseAdapter(cbPlugin));
+            lbls.put(pi, lblPlugin);
 
             gbc.gridx = 0;
             gbc.gridy = ++row;
@@ -170,6 +185,7 @@
             add(description, gbc);
         }
         pluginListInitialized = true;
+        return lbls;
     }
 
     /**
@@ -236,4 +252,50 @@
     public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
         return visibleRect.height;
     }
+
+    @Override
+    public List<SearchItem> getSearchItemsAsync() {
+
+        //if (model.getAvailablePlugins().isEmpty()) {
+
+        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(new PleaseWaitProgressMonitor(true));
+        task.run();
+        model.setAvailablePlugins(task.getAvailablePlugins());
+
+        if (!task.hasSiteCacheFile()) {
+            Collection<String> pluginSites = Preferences.main().getOnlinePluginSites();
+            if (!pluginSites.isEmpty()) {
+                final ReadRemotePluginInformationTask remoteTask = new ReadRemotePluginInformationTask(
+                        new PleaseWaitProgressMonitor(true), pluginSites, false);
+                remoteTask.run();
+                model.updateAvailablePlugins(remoteTask.getAvailablePlugins());
+            }
+        }
+
+        Map<PluginInformation, JLabel> map = GuiHelper.runInEDTAndWaitAndReturn(() -> {
+            removeAll();
+            displayEmptyPluginListInformation();
+            return displayPluginList(model.getAvailablePlugins());
+        });
+        pluginListInitialized = true;
+
+        return map.entrySet().stream().map((entry) -> {
+            String v = "";
+            PluginInformation plugin = entry.getKey();
+            if (plugin.localversion != null && !plugin.localversion.trim().isEmpty()) {
+                v = " (local: " + plugin.localversion + ")";
+            }
+            return new SearchItem(entry.getValue(), plugin.getName() + v, plugin.description, true, false);
+        }).collect(Collectors.toList());
+
+        //}
+        /*return model.getAvailablePlugins().stream().map(plugin -> {
+            String v = "";
+            if (plugin.localversion != null && !plugin.localversion.trim().isEmpty()) {
+                v = " (local: " + plugin.localversion + ")";
+            }
+            return new SearchItem(null, plugin.getName() + v, plugin.description, true, false);
+        }).collect(Collectors.toList());*/
+    }
+
 }
Index: src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java	(working copy)
@@ -778,4 +778,10 @@
         }
         return false;
     }
+
+    /*@Override
+    public List<SearchItem> getSearchItemsAsync() {
+        //layerInfo.load(true);
+        return null;
+    }*/
 }
Index: src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java
===================================================================
--- src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java	(revision 17917)
+++ src/org/openstreetmap/josm/data/preferences/sources/ExtendedSourceEntry.java	(working copy)
@@ -49,7 +49,7 @@
     }
 
     private static void appendRow(StringBuilder s, String th, String td) {
-        s.append("<tr><th>").append(th).append("</th><td>").append(Utils.escapeReservedCharactersHTML(td)).append("</td</tr>");
+        s.append("<tr><th>").append(th).append("</th><td>").append(Utils.escapeReservedCharactersHTML(td)).append("</td></tr>");
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java	(working copy)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+
+/**
+ * TextField for searching the preferences
+ * @author Bjoeni
+ */
+public class SearchTextField extends JosmTextField {
+    private SearchPanel panel;
+    private PreferenceTabbedPane tabs;
+
+    public SearchTextField(SearchPanel panel) {
+
+        this.panel = panel;
+        tabs = panel.getTabPane();
+        setHint(tr("Search..."));
+        getDocument().addDocumentListener(new DocumentListener() {
+
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                panel.getTabPane().setSelectedIndex(0);
+                panel.search(getTextContent());
+            }
+
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                panel.getTabPane().setSelectedIndex(0);
+                panel.search(getTextContent());
+
+            }
+
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+            }
+        });
+    }
+
+    public void adjustWidth() {
+        int width = getPreferredSize().width;
+        for (int i = 1; i < tabs.getTabCount(); i++) {
+            width = Math.max(width, tabs.getBoundsAt(i).width); //TODO all width the same, only check one?
+        }
+        setPreferredSize(new Dimension(width, getPreferredSize().height));
+    }
+
+    @Override
+    public String getText() {
+        return tr("Search");
+    }
+
+    public String getTextContent() {
+        return super.getText();
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java	(working copy)
@@ -0,0 +1,75 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class SearchFilters extends ArrayList<String> {
+    private final List<String> searchStrsNormalized;
+
+    public SearchFilters(String filterStr) {
+        String[] arr = filterStr.split(" ");
+        this.addAll(Arrays.asList(arr));
+        this.searchStrsNormalized = Arrays.stream(arr).map(SearchFilters::normalize).collect(Collectors.toList());
+    }
+
+    public List<String> getNormalized() {
+        return searchStrsNormalized;
+    }
+
+    /**
+     * Highlights the search string in the given item text.
+     * Needs to be done manually to deal with accents/umlauts
+     * @param itemStr
+     * @param itemStrNormalized
+     * @return
+     */
+    public String highlightString(String itemStr, String itemStrNormalized) {
+        Pattern p = Pattern.compile("(?i)(" + String.join("|", searchStrsNormalized) + ")");
+        Matcher m = p.matcher(itemStrNormalized);
+        StringBuilder sb = null;
+        int i = 0;
+
+        while (m.find()) {
+            if (sb == null) {
+                sb = new StringBuilder("<html>");
+            }
+            sb.append(itemStr.substring(i, m.start()))
+                .append("<span style=\"color: ")
+                .append(SearchPanel.DARK_MODE ? "yellow" : "red")
+                .append("\">")
+                .append(itemStr.substring(m.start(), m.end()))
+                .append("</span>");
+            i = m.end();
+        }
+
+        if (sb == null) {
+            return itemStr;
+        }
+
+        sb.append(itemStr.substring(i))
+            .append("</html>");
+        return sb.toString();
+    }
+
+    public static String normalize(String str) {
+        if (str == null)
+            return null;
+
+        String strLower = str.toLowerCase(SearchIndex.CURRENT_LOCALE);
+
+        String ret = Normalizer.normalize(strLower, Normalizer.Form.NFD)
+                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
+
+        if (ret.length() != str.length()) {
+            return strLower;
+        }
+
+        return ret;
+    }
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/progress/swing/PleaseWaitProgressMonitor.java
===================================================================
--- src/org/openstreetmap/josm/gui/progress/swing/PleaseWaitProgressMonitor.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/progress/swing/PleaseWaitProgressMonitor.java	(working copy)
@@ -96,6 +96,8 @@
 
     private boolean cancelable;
 
+    private boolean silent;
+
     /**
      * Returns the progress monitor being currently displayed.
      * @return the progress monitor being currently displayed
@@ -197,6 +199,16 @@
         this.windowTitle = windowTitle;
     }
 
+    public PleaseWaitProgressMonitor(boolean silent) {
+        this();
+        this.silent = silent;
+    }
+
+    public PleaseWaitProgressMonitor(String title, boolean silent) {
+        this(title);
+        this.silent = silent;
+    }
+
     private final ActionListener cancelListener = e -> cancel();
 
     private final ActionListener inBackgroundListener = e -> {
@@ -234,7 +246,7 @@
     public void doBeginTask() {
         doInEDT(() -> {
             currentProgressMonitor = this;
-            if (GraphicsEnvironment.isHeadless()) {
+            if (silent || GraphicsEnvironment.isHeadless()) {
                 return;
             }
             if (dialogParent != null && dialog == null) {
Index: src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(working copy)
@@ -49,6 +49,7 @@
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.search.NotSearchablePanel;
 import org.openstreetmap.josm.gui.util.DocumentAdapter;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
@@ -163,8 +164,11 @@
 
     @Override
     public void addGui(final PreferenceTabbedPane gui) {
-        JPanel p = gui.createPreferenceTab(this);
+        JPanel panel = gui.createPreferenceTab(this);
 
+        NotSearchablePanel p = new NotSearchablePanel(new GridBagLayout());
+        panel.add(p, GBC.std().fill());
+
         final JPanel txtFilterPanel = new JPanel(new GridBagLayout());
         p.add(txtFilterPanel, GBC.eol().fill(GBC.HORIZONTAL));
         txtFilter = new FilterField();
Index: src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java	(working copy)
@@ -0,0 +1,56 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.LayoutManager;
+
+import javax.swing.JPanel;
+
+/**
+ * {@link JPanel} that disallows searching its contents
+ * @see ISearchableComponent
+ * @author Bjoeni
+ */
+public class NotSearchablePanel extends JPanel implements ISearchableComponent {
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     */
+    public NotSearchablePanel() {
+        super();
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param layout
+     */
+    public NotSearchablePanel(LayoutManager layout) {
+        super(layout);
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param isDoubleBuffered
+     */
+    public NotSearchablePanel(boolean isDoubleBuffered) {
+        super(isDoubleBuffered);
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param layout
+     * @param isDoubleBuffered
+     */
+    public NotSearchablePanel(LayoutManager layout, boolean isDoubleBuffered) {
+        super(layout, isDoubleBuffered);
+    }
+
+    /**
+     * The component can not be searched
+     * @return false
+     */
+    @Override
+    public boolean isSearchable() {
+        return false;
+    }
+
+}
Index: src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java
===================================================================
--- src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(revision 17917)
+++ src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(working copy)
@@ -39,6 +39,7 @@
 public class ReadLocalPluginInformationTask extends PleaseWaitRunnable {
     private final Map<String, PluginInformation> availablePlugins;
     private boolean canceled;
+    private boolean hasSiteCacheFile;
 
     /**
      * Constructs a new {@code ReadLocalPluginInformationTask}.
@@ -96,6 +97,7 @@
             monitor.setCustomText(tr("Processing file ''{0}''", fname));
             try {
                 processLocalPluginInformationFile(f);
+                hasSiteCacheFile = true;
             } catch (PluginListParseException e) {
                 Logging.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname));
                 Logging.error(e);
@@ -219,6 +221,13 @@
     }
 
     /**
+     * @return true if a local cache file was parsed
+     */
+    public boolean hasSiteCacheFile() {
+        return hasSiteCacheFile;
+    }
+
+    /**
      * Replies true if the task was canceled by the user
      *
      * @return true if the task was canceled by the user
Index: src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 17917)
+++ src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(working copy)
@@ -10,6 +10,8 @@
 import java.awt.FontMetrics;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
 import java.util.ArrayList;
@@ -60,6 +62,8 @@
 import org.openstreetmap.josm.gui.preferences.plugin.PluginPreference;
 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
 import org.openstreetmap.josm.gui.preferences.remotecontrol.RemoteControlPreference;
+import org.openstreetmap.josm.gui.preferences.search.SearchPanel;
+import org.openstreetmap.josm.gui.preferences.search.SearchTextField;
 import org.openstreetmap.josm.gui.preferences.server.ProxyPreference;
 import org.openstreetmap.josm.gui.preferences.server.ServerAccessPreference;
 import org.openstreetmap.josm.gui.preferences.shortcut.ShortcutPreference;
@@ -204,7 +208,7 @@
         boolean validatePreferences();
     }
 
-    private interface PreferenceTab {
+    public interface PreferenceTab {
         TabPreferenceSetting getTabPreferenceSetting();
 
         Component getComponent();
@@ -280,6 +284,9 @@
     private static final PreferenceSettingFactory ADVANCED_PREFERENCE_FACTORY = new AdvancedPreference.Factory();
     private final transient List<PreferenceSetting> settings = new ArrayList<>();
 
+    public final SearchPanel searchPanel = new SearchPanel(this);
+    private final SearchTextField searchTextField = new SearchTextField(searchPanel);
+
     // distinct list of tabs that have been initialized (we do not initialize tabs until they are displayed to speed up dialog startup)
     private final transient List<PreferenceSetting> settingsInitialized = new ArrayList<>();
 
@@ -464,7 +471,9 @@
      */
     public PreferenceTabbedPane() {
         super(SwingConstants.LEFT, JTabbedPane.SCROLL_TAB_LAYOUT);
-        super.addMouseWheelListener(new WheelListener(this));
+        MouseAndWheelListener l = new MouseAndWheelListener(this);
+        super.addMouseWheelListener(l);
+        super.addMouseListener(l);
         ExpertToggleAction.addExpertModeChangeListener(this);
     }
 
@@ -525,8 +534,14 @@
     private void addGUITabs(boolean clear) {
         boolean expert = ExpertToggleAction.isExpert();
         if (clear) {
+            /*for (int i = 1; i < getTabCount(); i++) {
+                remove(i);
+            }*/
             removeAll();
         }
+
+        addTab(null, searchPanel);
+        setTabComponentAt(0, searchTextField);
         // Compute max tab length in pixels
         int maxWidth = computeMaxTabWidth();
         // Inspect each tab setting
@@ -558,7 +573,8 @@
                 Logging.debug("{0}: hiding empty {1}", getClass().getSimpleName(), tps);
             });
         }
-        setSelectedIndex(-1);
+        searchTextField.adjustWidth();
+        //setSelectedIndex(2);
     }
 
     private int computeMaxTabWidth() {
@@ -627,11 +643,11 @@
      * This mouse wheel listener reacts when a scroll is carried out over the
      * tab strip and scrolls one tab/down or up, selecting it immediately.
      */
-    static final class WheelListener implements MouseWheelListener {
+    static final class MouseAndWheelListener implements MouseWheelListener, MouseListener {
 
         final JTabbedPane tabbedPane;
 
-        WheelListener(JTabbedPane tabbedPane) {
+        MouseAndWheelListener(JTabbedPane tabbedPane) {
             this.tabbedPane = tabbedPane;
         }
 
@@ -647,6 +663,27 @@
 
             tabbedPane.setSelectedIndex(newTab);
         }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            tabbedPane.requestFocus();
+        }
+
+        @Override
+        public void mouseEntered(MouseEvent e) {
+        }
+
+        @Override
+        public void mouseExited(MouseEvent e) {
+        }
+
+        @Override
+        public void mousePressed(MouseEvent e) {
+        }
+
+        @Override
+        public void mouseReleased(MouseEvent e) {
+        }
     }
 
     @Override
@@ -654,38 +691,41 @@
         int index = getSelectedIndex();
         Component sel = getSelectedComponent();
         if (index > -1 && sel instanceof PreferenceTab) {
-            PreferenceTab tab = (PreferenceTab) sel;
-            TabPreferenceSetting preferenceSettings = tab.getTabPreferenceSetting();
-            if (!settingsInitialized.contains(preferenceSettings)) {
-                try {
-                    getModel().removeChangeListener(this);
-                    preferenceSettings.addGui(this);
-                    // Add GUI for sub preferences
-                    for (PreferenceSetting setting : settings) {
-                        if (setting instanceof SubPreferenceSetting) {
-                            addSubPreferenceSetting(preferenceSettings, (SubPreferenceSetting) setting);
-                        }
+            initializeTab(index, (PreferenceTab) sel, false);
+        }
+    }
+
+    public void initializeTab(int index, PreferenceTab tab, boolean silent) {
+        TabPreferenceSetting preferenceSettings = tab.getTabPreferenceSetting();
+        if (!settingsInitialized.contains(preferenceSettings)) {
+            try {
+                getModel().removeChangeListener(this);
+                preferenceSettings.addGui(this);
+                // Add GUI for sub preferences
+                for (PreferenceSetting setting : settings) {
+                    if (setting instanceof SubPreferenceSetting) {
+                        addSubPreferenceSetting(preferenceSettings, (SubPreferenceSetting) setting);
                     }
-                    Icon icon = getIconAt(index);
-                    remove(index);
-                    if (index <= insertGUITabsForSetting(icon, preferenceSettings, index, computeMaxTabWidth())) {
-                        setSelectedIndex(index);
-                    }
-                } catch (SecurityException ex) {
-                    Logging.error(ex);
-                } catch (RuntimeException ex) { // NOPMD
-                    // allow to change most settings even if e.g. a plugin fails
-                    BugReportExceptionHandler.handleException(ex);
-                } finally {
-                    settingsInitialized.add(preferenceSettings);
-                    getModel().addChangeListener(this);
                 }
+                Icon icon = getIconAt(index);
+                remove(index);
+                if (index <= insertGUITabsForSetting(icon, preferenceSettings, index, computeMaxTabWidth()) && !silent) {
+                    setSelectedIndex(index);
+                }
+            } catch (SecurityException ex) {
+                Logging.error(ex);
+            } catch (RuntimeException ex) { // NOPMD
+                // allow to change most settings even if e.g. a plugin fails
+                BugReportExceptionHandler.handleException(ex);
+            } finally {
+                settingsInitialized.add(preferenceSettings);
+                getModel().addChangeListener(this);
             }
-            Container ancestor = getTopLevelAncestor();
-            if (ancestor instanceof PreferenceDialog) {
-                ((PreferenceDialog) ancestor).setHelpContext(preferenceSettings.getHelpContext());
-            }
         }
+        Container ancestor = getTopLevelAncestor();
+        if (ancestor instanceof PreferenceDialog) {
+            ((PreferenceDialog) ancestor).setHelpContext(preferenceSettings.getHelpContext());
+        }
     }
 
     private void addSubPreferenceSetting(TabPreferenceSetting preferenceSettings, SubPreferenceSetting sps) {
Index: src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/ISearchableComponent.java	(working copy)
@@ -0,0 +1,60 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.util.List;
+
+/**
+ * Interface allowing components in the preferences to determine if and how they can be searched
+ * @author Bjoeni
+ */
+public interface ISearchableComponent {
+    /**
+     * @return whether the component can be searched<br><br>
+     *
+     * default: true
+     */
+    default boolean isSearchable() {
+        return true;
+    }
+
+    /**
+     * @return whether the children of this component should be traversed <br><br>
+     *
+     * default: same as {@link #isSearchable()} unless explicitly overridden
+     */
+    default boolean isChildrenSearchable() {
+        return isSearchable();
+    }
+
+    /**
+     * Called from the EventDispatchThread (GUI), should not be blocked.
+     *
+     * @return the {@link SearchItem}s that should be used. Ignored if {@link #isSearchable()} returns {@code false}.<br>
+     * Overrides the default {@link SearchTextFinder} if returning not {@code null} (including if an empty list is returned)<br><br>
+     *
+     * default: null
+     */
+    default List<SearchItem> getSearchItems() {
+        return null;
+    }
+
+    /**
+     * Called from the worker thread, can be blocked as long as necessary. Calls to the GUI must be placed in the EventDispatchThread.
+     *
+     * @return the {@link SearchItem}s that should be used. Ignored if {@link #isSearchable()} returns {@code false}.<br>
+     * Overrides the default {@link SearchTextFinder} if returning not {@code null} (including if an empty list is returned)<br><br>
+     *
+     * default: null
+     */
+    default List<SearchItem> getSearchItemsAsync() {
+        return null;
+    }
+
+    /**
+     * @return
+     */
+    default String getIconName() {
+        return null;
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/NotSearchablePanel.java	(working copy)
@@ -0,0 +1,56 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.LayoutManager;
+
+import javax.swing.JPanel;
+
+/**
+ * {@link JPanel} that disallows searching its contents
+ * @see ISearchableComponent
+ * @author Bjoeni
+ */
+public class NotSearchablePanel extends JPanel implements ISearchableComponent {
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     */
+    public NotSearchablePanel() {
+        super();
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param layout
+     */
+    public NotSearchablePanel(LayoutManager layout) {
+        super(layout);
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param isDoubleBuffered
+     */
+    public NotSearchablePanel(boolean isDoubleBuffered) {
+        super(isDoubleBuffered);
+    }
+
+    /**
+     * Create {@link JPanel} that disallows searching its contents
+     * @param layout
+     * @param isDoubleBuffered
+     */
+    public NotSearchablePanel(LayoutManager layout, boolean isDoubleBuffered) {
+        super(layout, isDoubleBuffered);
+    }
+
+    /**
+     * The component can not be searched
+     * @return false
+     */
+    @Override
+    public boolean isSearchable() {
+        return false;
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchFilters.java	(working copy)
@@ -0,0 +1,75 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class SearchFilters extends ArrayList<String> {
+    private final List<String> searchStrsNormalized;
+
+    public SearchFilters(String filterStr) {
+        String[] arr = filterStr.split(" ");
+        this.addAll(Arrays.asList(arr));
+        this.searchStrsNormalized = Arrays.stream(arr).map(SearchFilters::normalize).collect(Collectors.toList());
+    }
+
+    public List<String> getNormalized() {
+        return searchStrsNormalized;
+    }
+
+    /**
+     * Highlights the search string in the given item text.
+     * Needs to be done manually to deal with accents/umlauts
+     * @param itemStr
+     * @param itemStrNormalized
+     * @return
+     */
+    public String highlightString(String itemStr, String itemStrNormalized) {
+        Pattern p = Pattern.compile("(?i)(" + String.join("|", searchStrsNormalized) + ")");
+        Matcher m = p.matcher(itemStrNormalized);
+        StringBuilder sb = null;
+        int i = 0;
+
+        while (m.find()) {
+            if (sb == null) {
+                sb = new StringBuilder("<html>");
+            }
+            sb.append(itemStr.substring(i, m.start()))
+                .append("<span style=\"color: ")
+                .append(SearchPanel.DARK_MODE ? "yellow" : "red")
+                .append("\">")
+                .append(itemStr.substring(m.start(), m.end()))
+                .append("</span>");
+            i = m.end();
+        }
+
+        if (sb == null) {
+            return itemStr;
+        }
+
+        sb.append(itemStr.substring(i))
+            .append("</html>");
+        return sb.toString();
+    }
+
+    public static String normalize(String str) {
+        if (str == null)
+            return null;
+
+        String strLower = str.toLowerCase(SearchIndex.CURRENT_LOCALE);
+
+        String ret = Normalizer.normalize(strLower, Normalizer.Form.NFD)
+                .replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
+
+        if (ret.length() != str.length()) {
+            return strLower;
+        }
+
+        return ret;
+    }
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchIndex.java	(working copy)
@@ -0,0 +1,304 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.LayoutManager;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.stream.Collectors;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferenceTab;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.LanguageInfo;
+
+/**
+ * Contains all {@link SearchItem}s
+ * @author Bjoeni
+ */
+public class SearchIndex {
+    public static final Locale CURRENT_LOCALE = LanguageInfo.getLocale(LanguageInfo.getJOSMLocaleCode());
+    public static final GBC EOL = GBC.eol();
+
+    Stack<SearchItem> stack = new Stack<>();
+    List<SearchItem> parents = new ArrayList<>();
+    PreferenceTabbedPane prefTabs;
+    GridBagConstraints lastConstraint;
+    private boolean built;
+    private String lastFilterStr;
+    private int currentTabIndex = -1;
+
+    private JPanel panel = GuiHelper.runInEDTAndWaitAndReturn(() -> new JPanel(new GridBagLayout()));
+
+    GridBagConstraints gbc_nw;
+
+    public SearchIndex(PreferenceTabbedPane prefTabs) {
+        this.prefTabs = prefTabs;
+        gbc_nw = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc_nw.anchor = GBC.NORTHWEST;
+    }
+
+    void add(SearchItem item) {
+        if (item.getText() == null)
+            return;
+
+        item.setTabIndex(prefTabs, currentTabIndex);
+        if (stack.isEmpty()) {
+            item.setLevelInset(0, 0);
+            parents.add(item);
+            GuiHelper.runInEDT(() -> {
+                getPanel().add(item.getComponent(), gbc_nw);
+            });
+            stack.push(item);
+        } else {
+            SearchItem lastItem = stack.lastElement();
+            if (!lastItem.isEOL() && lastItem.level == item.level) {
+                lastItem.setEOL(item.isEOL());
+                lastItem.addChild(item);
+            } else if (lastItem.level < item.level
+                    || (lastItem.level == item.level && lastItem.inset + 15 < item.inset)) {
+                stack.push(lastItem.addChild(item));
+            } else {
+                stack.pop();
+                add(item);
+            }
+        }
+    }
+
+    void addAll(List<SearchItem> items, boolean isEOL) {
+        if (items.size() > 0) {
+            items.get(items.size() - 1).setEOL(isEOL);
+        }
+        items.forEach(item -> add(item));
+    }
+
+    void insertEOL() {
+        if (!stack.isEmpty()) {
+            stack.lastElement().setEOL();
+        }
+    }
+
+    void insertSeparator() {
+        if (!stack.isEmpty()) {
+            stack.lastElement().maximizeInset();
+        }
+    }
+
+    void insertNewPage() {
+        stack.clear();
+    }
+
+    public boolean isBuilt() {
+        return built;
+    }
+
+    /**
+     * @throws InvocationTargetException exception while initializing tab
+     * @throws InterruptedException thread interrupted
+     */
+    public void build() throws InvocationTargetException, InterruptedException {
+        //setVisible(false);
+        built = false;
+        parents.clear();
+        getPanel().removeAll();
+        for (int i = 0; i < prefTabs.getTabCount(); i++) {
+            Component c = prefTabs.getComponentAt(i);
+            currentTabIndex = i;
+            String iconName = null;
+            if (c instanceof PreferenceTab) {
+                iconName = ((PreferenceTab) c).getTabPreferenceSetting().getIconName();
+                final int index = i;
+                final Component comp = c;
+                SwingUtilities.invokeAndWait(() -> prefTabs.initializeTab(index, (PreferenceTab) comp, true));
+                c = prefTabs.getComponentAt(i);
+            }
+            insertNewPage();
+            searchComponent(c, 1, iconName);
+        }
+
+        built = true;
+        //setVisible(true);
+    }
+
+    public boolean filter(String filterStr) {
+        boolean found = false;
+
+        if (lastFilterStr != null && filterStr.indexOf(lastFilterStr) != 0) {
+            parents.forEach(SearchItem::showAll);
+        }
+
+        if (!filterStr.isEmpty()) {
+            SearchFilters filters = new SearchFilters(filterStr);
+            found = parents.stream()
+                    .filter(SearchItem::isVisible)
+                    .map(item -> item.filter(filters))
+                    .collect(Collectors.toList()) //force all visible elements to be evaluated
+                    .contains(true);
+        }
+
+        lastFilterStr = filterStr;
+        return found;
+    }
+
+    /*public DefaultMutableTreeNode getTreeView() {
+        return tree;
+    }
+
+    public synchronized DefaultMutableTreeNode createTreeView(DefaultTreeModel model) {
+        tree.removeAllChildren();
+        parents.forEach(child -> {
+            MutableTreeNode t = child.createTreeView(model);
+            if (t != null) {
+                model.insertNodeInto(t, tree, tree.getChildCount());
+                //tree.add(t);
+            }
+        });
+        return tree;
+    }*/
+
+    private boolean searchComponent(Component comp, int level, String iconName)
+            throws InvocationTargetException, InterruptedException {
+
+        final Component c = comp;
+
+        boolean isEOL = true;
+        GridBagConstraints currentConstraint = null;
+
+        Container p = c.getParent();
+        if (p != null) {
+            LayoutManager layout = p.getLayout();
+            if (layout != null && layout instanceof GridBagLayout) {
+                GridBagLayout grid = (GridBagLayout) layout;
+
+                currentConstraint = grid.getConstraints(c);
+                isEOL = currentConstraint.gridwidth == GridBagConstraints.REMAINDER;
+            }
+        }
+
+        if (lastConstraint != null && currentConstraint != null
+                && (((lastConstraint.fill == GridBagConstraints.HORIZONTAL
+                        || lastConstraint.fill == GridBagConstraints.BOTH)
+                        && currentConstraint.fill != GridBagConstraints.HORIZONTAL
+                        && currentConstraint.fill != GridBagConstraints.BOTH)
+                        || lastConstraint.gridy != currentConstraint.gridy)) {
+            insertEOL();
+        }
+
+        lastConstraint = currentConstraint;
+        boolean isChildrenSearchable = true;
+        ISearchableComponent s = null;
+        if (c instanceof ISearchableComponent) {
+            s = (ISearchableComponent) c;
+            isChildrenSearchable = s.isChildrenSearchable();
+        }
+
+        if (s == null || s.isSearchable()) {
+
+            List<SearchItem> items = null;
+            if (s != null) {
+                items = s.getSearchItemsAsync();
+                final ISearchableComponent s2 = s;
+                List<SearchItem> itm2 = GuiHelper.runInEDTAndWaitAndReturn(() -> s2.getSearchItems());
+                if (itm2 != null) {
+                    if (items == null) {
+                        items = itm2;
+                    } else {
+                        items.addAll(itm2);
+                    }
+                }
+
+                if (iconName == null) {
+                    iconName = s.getIconName();
+                }
+
+                /*final SwingTransferItem swing = new SwingTransferItem(s);
+                SwingUtilities.invokeAndWait(() -> {
+                    swing.list = swing.component.getSearchItems();
+                });
+
+                if (swing.list != null) {
+                    items = swing.list;
+                }*/
+            }
+            if (items == null) {
+                final List<SearchItem> itm = new ArrayList<>();
+                SwingUtilities.invokeAndWait(() -> {
+                    SearchTextFinder.DEFAULT_SEARCH_TEXT_FINDERS
+                            .forEach(finder -> itm.addAll(finder.getSearchItems(c)));
+                });
+
+                items = itm;
+            }
+            if (iconName != null) {
+                final String iconName2 = iconName;
+                items.forEach(item -> item.setIconName(iconName2));
+            }
+            if (items.size() == 0) {
+                if (isEOL) {
+                    insertEOL();
+                }
+                if (c instanceof JSeparator) {
+                    insertSeparator();
+                }
+
+            } else {
+                items.get(0).addOriginalComponent(c);
+                if (c instanceof JComponent) {
+                    items.get(0).setTooltip(((JComponent) c).getToolTipText());
+                }
+
+                final int inset = currentConstraint == null ? 0 : currentConstraint.insets.left;
+                items.forEach(item -> item.setLevelInset(level, inset));
+                isChildrenSearchable = isChildrenSearchable
+                        && items.stream().allMatch(item -> item.isChildComponentsSearchable());
+
+                addAll(items, isEOL);
+            }
+        }
+
+        if (isChildrenSearchable && c instanceof Container) {
+            Container cont = (Container) c;
+            List<Component> components = Arrays.asList(cont.getComponents());
+            if (components.size() > 0) {
+                insertEOL();
+                for (Component component : components) {
+                    searchComponent(component, level + 1, iconName);
+                }
+            }
+        }
+        return isEOL;
+    }
+
+    /**
+     * @return the panel
+     */
+    public JPanel getPanel() {
+        return panel;
+    }
+
+    public void setVisible(boolean visible) {
+        GuiHelper.runInEDT(() -> {
+            panel.setVisible(visible);
+        });
+    }
+
+    public void updateComponents() throws InvocationTargetException, InterruptedException {
+        for (SearchItem item : parents) {
+            item.updateComponents();
+        }
+    }
+
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchItem.java	(working copy)
@@ -0,0 +1,290 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.swing.ImageIcon;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Contains searchable items and their components
+ * @author Bjoeni
+ */
+public class SearchItem {
+    int level;
+    int inset;
+    private String text;
+    private String tooltip;
+    private String highlight;
+    private List<SearchItem> children = new ArrayList<>();
+    private List<Component> originalComponents = new ArrayList<>();
+
+    private int tabIndex;
+    private boolean eol = true;
+    private boolean visible = true;
+    private boolean hasVisibleChildren;
+    private boolean childComponentsSearchable = true;
+    private String iconName;
+    private String textNormalized;
+    private String tooltipNormalized;
+    private PreferenceTabbedPane tabs;
+    private SearchItemComponent comp;
+
+    public SearchItem(String text) {
+        this.setText(text);
+    }
+
+    public SearchItem(Component originalComponent, String text, String tooltip, boolean eol,
+            boolean childComponentsSearchable) {
+        this.originalComponents.add(originalComponent);
+        this.setText(text);
+        this.setTooltip(tooltip);
+        this.setEOL(eol);
+        this.childComponentsSearchable = childComponentsSearchable;
+    }
+
+    public SearchItem addChild(SearchItem item) {
+        synchronized (children) {
+            Optional<SearchItem> match = children.stream()
+                    .filter(c -> Objects.equals(c.text, item.text) && c.level == item.level && c.inset == item.inset)
+                    .findAny();
+            if (match.isPresent()) {
+                match.get().merge(item);
+                return match.get();
+            } else {
+                children.add(item);
+                GuiHelper.runInEDT(() -> {
+                    getComponent().addChild(item.getComponent());
+                });
+                return item;
+            }
+        }
+
+    }
+
+    public void merge(SearchItem item) {
+        this.originalComponents.addAll(item.originalComponents);
+        this.setEOL(this.isEOL() || item.isEOL());
+    }
+
+    public boolean filter(SearchFilters filters) {
+        visible = this.matches(filters);
+        hasVisibleChildren = false;
+        for (SearchItem child : children) {
+            if (child.isVisible()) {
+                hasVisibleChildren = child.filter(filters) || hasVisibleChildren;
+            }
+        }
+        visible = visible || hasVisibleChildren;
+
+        if (visible && text != null && textNormalized != null) {
+            highlight = filters.highlightString(text, textNormalized);
+        } else {
+            highlight = "<html><span style=\"color: red;\"><b>ERROR</b></span></html>";
+        }
+        return visible;
+    }
+
+    public boolean matches(SearchFilters filters) {
+        return filters.getNormalized().stream()
+                .allMatch(str -> textNormalized != null && textNormalized.indexOf(str) != -1
+                        || (tooltipNormalized != null && tooltipNormalized.indexOf(str) != -1));
+    }
+
+    public void showAll() {
+        visible = true;
+        children.forEach(SearchItem::showAll);
+    }
+
+    @Override
+    public String toString() {
+        return text + (tooltip == null ? "" : " (Tooltip: " + tooltip + ")") + " [" + level + "." + inset
+                + (eol ? ":EOL" : "") + "] {" + originalComponents.size() + "}";
+    }
+
+    public ImageIcon getIcon() {
+        if (iconName == null)
+            return null;
+
+        return iconName == null || iconName.isEmpty() ? null
+                : iconName.contains("/") ? ImageProvider.get(iconName, ImageSizes.SMALLICON)
+                        : ImageProvider.get("preferences", iconName, ImageSizes.SMALLICON);
+    }
+
+    public boolean isVisible() {
+        return visible;
+    }
+
+    public boolean isEOL() {
+        return eol;
+    }
+
+    public void setEOL() {
+        setEOL(true);
+    }
+
+    public void setEOL(boolean eol) {
+        this.eol = eol;
+    }
+
+    public void setLevelInset(int level, int inset) {
+        this.level = level;
+        this.inset = inset;
+    }
+
+    public void maximizeInset() {
+        this.inset = Integer.MAX_VALUE;
+    }
+
+    public void setTabIndex(PreferenceTabbedPane tabs, int tabIndex) {
+        this.tabs = tabs;
+        this.tabIndex = tabIndex;
+    }
+
+    public void setIconName(String iconName) {
+        this.iconName = iconName;
+    }
+
+    public void addOriginalComponent(Component c) {
+        this.originalComponents.add(c);
+    }
+
+    public void showOriginalComponent() {
+        if (tabIndex > -1) {
+            tabs.setSelectedIndex(tabIndex);
+        }
+        originalComponents.stream().filter(Objects::nonNull).forEach(comp -> {
+            //Color bg = comp.getBackground();
+            Color fg = comp.getForeground();
+            //Logging.debug("BG:" + bg.toString());
+            Logging.debug("FG:" + fg.toString());
+            //comp.setBackground(SystemColor.text);
+            comp.setForeground(SearchPanel.DARK_MODE ? Color.yellow : Color.red);
+            comp.requestFocus();
+            if (comp instanceof JComponent) {
+                JComponent jcomp = (JComponent) comp;
+                Rectangle bounds = new Rectangle(jcomp.getBounds());
+                jcomp.scrollRectToVisible(bounds);
+            }
+            Timer timer = new Timer(3000, l -> {
+                //comp.setBackground(bg);
+                comp.setForeground(fg);
+            });
+            timer.setRepeats(false);
+            timer.start();
+        });
+        if (originalComponents.stream().filter(Objects::nonNull).noneMatch(Component::isVisible)) {
+            Logging.warn("INVIS!!");
+        }
+    }
+
+    /**
+     * @return the component
+     */
+    public SearchItemComponent getComponent() {
+        if (comp == null) {
+            comp = GuiHelper.runInEDTAndWaitAndReturn(() -> new SearchItemComponent(this));
+        }
+        return comp;
+    }
+
+    public void updateComponents() throws InvocationTargetException, InterruptedException {
+        SwingUtilities.invokeAndWait(() -> {
+            //invokeLater is faster but causes too many calls at once and blocks the UI
+            SearchItemComponent c = getComponent();
+            c.setVisible(this.isVisible());
+            if (isVisible()) {
+                c.setToolTipText(toString());
+                c.setText(highlight);
+                c.setExpanded(true);
+            }
+        });
+        if (isVisible()) {
+            for (SearchItem child : children) {
+                child.updateComponents();
+            }
+        }
+    }
+
+    /**
+     * @return the hasVisibleChildren
+     */
+    public boolean hasVisibleChildren() {
+        return hasVisibleChildren;
+    }
+
+    /**
+     * @return the childComponentsSearchable
+     */
+    public boolean isChildComponentsSearchable() {
+        return childComponentsSearchable;
+    }
+
+    /**
+     * @return the text2
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * @param text the text to set
+     */
+    public void setText(String text) {
+        this.text = stripHtml(text);
+        this.textNormalized = SearchFilters.normalize(this.text);
+    }
+
+    /**
+     * @return the tooltip
+     */
+    public String getTooltip() {
+        return tooltip;
+    }
+
+    public void addTooltip(String tooltip) {
+        if (this.getTooltip() == null || this.getTooltip().trim().isEmpty()) {
+            this.setTooltip(tooltip);
+        }
+    }
+
+    /**
+     * @param tooltip the tooltip to set
+     */
+    public void setTooltip(String tooltip) {
+        this.tooltip = stripHtml(tooltip);
+        this.tooltipNormalized = SearchFilters.normalize(this.tooltip);
+    }
+
+    private static String stripHtml(String str) {
+        if (str == null || str.trim().length() == 0)
+            return null;
+
+        if (str.indexOf('<') > -1) {
+            str = str.replaceAll("(?i)(<br\\s/>\\s*)+", ": ").replaceAll("(<style>.*</style>|<[^<>]*>)", " ").trim()
+                    .replaceAll(" +", " ");
+        }
+
+        if (str.indexOf(':') == str.length() - 1) {
+            str = str.substring(0, str.length() - 1);
+        }
+
+        return str;
+    }
+
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchItemComponent.java	(working copy)
@@ -0,0 +1,151 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagLayout;
+import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
+
+public class SearchItemComponent extends JPanel {
+
+    private final GridBagLayout own_gbl = new GridBagLayout();
+    private final JLabel lbl = new JLabel();
+    private final JLabel expander = new JLabel();
+    private JPanel childPanel;
+    private final SearchItem item;
+
+    private GridBagLayout parent_gbl;
+    private boolean expanded = true;
+    private GBC gbc_nw;
+    private GBC gbc_nw_fill;
+
+    private static final ImageIcon ICON_EXPANDED = ImageProvider.get("dialogs", "down", ImageSizes.SMALLICON);
+    private static final ImageIcon ICON_COLLAPSED = ImageProvider.get("dialogs", "next", ImageSizes.SMALLICON);
+
+    private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
+    private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
+
+    public SearchItemComponent(SearchItem item) {
+        super(new GridBagLayout());
+        setVisible(false);
+        this.item = item;
+        add(expander);
+
+        gbc_nw = GBC.eol();
+        gbc_nw.anchor = GBC.NORTHWEST;
+        gbc_nw_fill = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc_nw_fill.anchor = GBC.NORTHWEST;
+        add(lbl, gbc_nw_fill);
+        lbl.setCursor(CURSOR_HAND);
+        lbl.setBackground(Color.cyan);
+        //lbl.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0));
+        setBorder(BorderFactory.createEmptyBorder(10, item.level == 0 ? 10 : 17, item.level == 0 ? 25 : 0, 10));
+
+        expander.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                setExpanded(!isExpanded());
+            }
+        });
+
+        addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                item.showOriginalComponent();
+            }
+        });
+    }
+
+    public void setText(String txt) {
+        lbl.setText(txt);
+    }
+
+    @Override
+    protected void paintComponent(Graphics graphics) {
+        super.paintComponent(graphics);
+
+        if (item.level != 0)
+            return;
+
+        Graphics2D g = (Graphics2D) graphics;
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+        int width = getWidth(),
+                height = getHeight(),
+                stroke = 1,
+                offset = 5,
+                radius = 15;
+
+
+        g.setColor(new Color(0, 0, 0, 125));
+        g.fillRoundRect(offset, offset, width - stroke - offset, height - stroke - offset - 10, radius, radius);
+
+        g.setColor(getBackground());
+        g.fillRoundRect(0, 0, width - 5, height - 15, radius, radius);
+
+        g.setColor(getForeground());
+        g.setStroke(new BasicStroke(stroke));
+        g.drawRoundRect(0, 0, width - 5, height - 15, radius, radius);
+
+        g.setStroke(new BasicStroke());
+    }
+
+    /**
+     * @return the expanded
+     */
+    public boolean isExpanded() {
+        return expanded;
+    }
+
+    /**
+     * @param expanded the expanded to set
+     */
+    public void setExpanded(boolean expanded) {
+        if (item.hasVisibleChildren()) {
+            this.expanded = expanded;
+            expander.setIcon(expanded ? ICON_EXPANDED : ICON_COLLAPSED);
+            expander.setCursor(CURSOR_HAND);
+            getChildPanel().setVisible(expanded);
+        } else {
+            expander.setIcon(ICON_COLLAPSED);
+        }
+    }
+
+    /**
+     * @return the childPanel
+     */
+    public JPanel getChildPanel() {
+        if (childPanel == null) {
+            childPanel = new JPanel(own_gbl);
+            add(childPanel, gbc_nw_fill);
+        }
+        return childPanel;
+    }
+
+    public void addChild(SearchItemComponent comp) {
+        getChildPanel().add(comp, gbc_nw_fill);
+        comp.setParentGBL(own_gbl);
+    }
+
+    /**
+     * @param parent_gbl the parent_gbl to set
+     */
+    public void setParentGBL(GridBagLayout parent_gbl) {
+        this.parent_gbl = parent_gbl;
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchPanel.java	(working copy)
@@ -0,0 +1,169 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.SystemColor;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Panel displaying search results in the preferences
+ * @author Bjoeni
+ */
+public class SearchPanel extends JScrollPane implements ISearchableComponent {
+
+    public static final boolean DARK_MODE = 130 < (Math.sqrt(Math.pow(SystemColor.text.getRed(), 2) * .241
+            + Math.pow(SystemColor.text.getGreen(), 2) * .691 + Math.pow(SystemColor.text.getBlue(), 2) * .068));
+
+    private PreferenceTabbedPane tabs;
+    private SearchIndex searchIndex;
+    private JLabel lblStatus = new JLabel(tr("Enter something in the text fied to start searching"));
+    private JPanel panel = new JPanel(new GridBagLayout());
+    private SearchThread thread;
+
+    public SearchPanel(PreferenceTabbedPane tabs) {
+        super(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        this.tabs = tabs;
+        searchIndex = new SearchIndex(tabs);
+        GBC gbc = GBC.eol().fill(GBC.HORIZONTAL);
+        gbc.anchor = GBC.NORTHWEST;
+        gbc.weightx = 1;
+        panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
+        panel.add(searchIndex.getPanel(), gbc);
+        gbc.gridy = 1;
+        lblStatus.setHorizontalAlignment(SwingConstants.CENTER);
+        gbc.fill();
+        gbc.anchor = GBC.CENTER;
+        panel.add(lblStatus, gbc);
+        //setAlignmentX(LEFT_ALIGNMENT);
+        setViewportView(panel);
+    }
+
+    public void search(String text) {
+        if (thread != null && thread.isAlive()) {
+            Logging.debug("alive");
+            thread.awaitBuildAndRestart(text);
+        } else {
+            Logging.debug("dead");
+            thread = new SearchThread(text);
+            thread.start();
+        }
+    }
+
+    public PreferenceTabbedPane getTabPane() {
+        return tabs;
+    }
+
+    public void destroyIndex() {
+        removeAll();
+        panel = null;
+        lblStatus = null;
+        setViewport(null);
+        this.searchIndex = null;
+    }
+
+    @Override
+    public boolean isSearchable() {
+        return false;
+    }
+
+    class SearchThread extends Thread {
+        private String text;
+        private StringBuilder searchableSettings;
+        private boolean building;
+        private boolean interrupted;
+        private String nextText;
+        private boolean justBuiltIndex;
+
+        public SearchThread(String text) {
+            this.text = text;
+        }
+
+        public SearchThread(String text, boolean justBuiltIndex) {
+            this.text = text;
+            this.justBuiltIndex = justBuiltIndex;
+        }
+
+        @Override
+        public void run() {
+            try {
+                boolean buildIndex = !searchIndex.isBuilt(); // || "".equals(text);
+                if (buildIndex) {
+                    building = true;
+                    Logging.info("Building search index...");
+                    setText(tr("Building search index..."));
+                    searchIndex.build();
+                    building = false;
+                    if (interrupted) {
+                        startNext(true);
+                        return;
+                    }
+                }
+
+                if (buildIndex || justBuiltIndex) {
+                    setText(tr("Searching..."));
+                }
+
+                //if (!"".equals(text)) {
+                Logging.debug("searching \"" + text + "\"");
+                boolean found = searchIndex.filter(text);
+                Logging.debug("searched \"" + text + "\"");
+                //}
+
+                Logging.debug("before \"" + text + "\"");
+                searchIndex.updateComponents();
+                Logging.debug("done \"" + text + "\"");
+
+                String txt = "";
+                if (!found) {
+                    txt += tr("No results found.") + "<br><br>";
+                }
+                if (false) { //expertmode
+                    txt += tr("You might get more results by enabling expert mode.");
+                }
+                setText(txt);
+
+            } catch (InterruptedException e) {
+                Logging.debug("Interrupted search thread \"" + text + "\"");
+                // to be expected
+            } catch (InvocationTargetException e) {
+                throw new JosmRuntimeException(e);
+            }
+        }
+
+        public void awaitBuildAndRestart(String nextText) {
+            this.nextText = nextText;
+            if (building) {
+                interrupted = true;
+            } else {
+                interrupt();
+                startNext(false);
+            }
+        }
+
+        private void startNext(boolean justBuiltIndex) {
+            thread = new SearchThread(nextText, justBuiltIndex);
+            thread.start();
+        }
+
+        private void setText(String txt) {
+            SwingUtilities.invokeLater(() -> {
+                lblStatus.setText(txt);
+            });
+        }
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchTextField.java	(working copy)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+
+/**
+ * TextField for searching the preferences
+ * @author Bjoeni
+ */
+public class SearchTextField extends JosmTextField {
+    private SearchPanel panel;
+    private PreferenceTabbedPane tabs;
+
+    public SearchTextField(SearchPanel panel) {
+
+        this.panel = panel;
+        tabs = panel.getTabPane();
+        setHint(tr("Search..."));
+        getDocument().addDocumentListener(new DocumentListener() {
+
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                panel.getTabPane().setSelectedIndex(0);
+                panel.search(getTextContent());
+            }
+
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                panel.getTabPane().setSelectedIndex(0);
+                panel.search(getTextContent());
+
+            }
+
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+            }
+        });
+    }
+
+    public void adjustWidth() {
+        int width = getPreferredSize().width;
+        for (int i = 1; i < tabs.getTabCount(); i++) {
+            width = Math.max(width, tabs.getBoundsAt(i).width); //TODO all width the same, only check one?
+        }
+        setPreferredSize(new Dimension(width, getPreferredSize().height));
+    }
+
+    @Override
+    public String getText() {
+        return tr("Search");
+    }
+
+    public String getTextContent() {
+        return super.getText();
+    }
+
+}
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java	(working copy)
@@ -0,0 +1,200 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JRadioButton;
+import javax.swing.JTable;
+import javax.swing.table.TableModel;
+
+/**
+ * Class for obtaining the searchable properties of arbitrary components
+ * @param <T> type of the component
+ * @author Bjoeni
+ */
+public class SearchTextFinder<T extends Component> {
+    List<IComponentProperties<T>> properties = new ArrayList<>();
+
+    @FunctionalInterface
+    private interface IComponentProperties<U extends Component> {
+        Object getFrom(U component);
+
+        default List<SearchItem> getSearchItems(U component) {
+            Object obj = getFrom(component);
+
+            if (obj == null) {
+                return new ArrayList<>();
+            }
+
+            if (obj instanceof String) {
+                return Arrays.asList(new SearchItem((String) obj));
+            }
+
+            if (obj instanceof String[]) {
+                return Arrays.asList((String[]) obj).stream().map(SearchItem::new).collect(Collectors.toList());
+            }
+
+            if (obj instanceof SearchItem[]) {
+                return Arrays.asList((SearchItem[]) obj);
+            }
+
+            throw new IllegalArgumentException();
+        }
+    }
+
+    @FunctionalInterface
+    interface ComponentProperty<U extends Component> extends IComponentProperties<U> {
+        @Override
+        String getFrom(U component);
+    }
+
+    @FunctionalInterface
+    interface ComponentPropertyArray<U extends Component> extends IComponentProperties<U> {
+        @Override
+        String[] getFrom(U component);
+    }
+
+    @FunctionalInterface
+    interface ComponentPropertyItem<U extends Component> extends IComponentProperties<U> {
+        @Override
+        SearchItem[] getFrom(U component);
+    }
+
+    /**
+     * Instantiates {@link SearchTextFinder}
+     */
+    public SearchTextFinder() {
+    }
+
+    /**
+     * Instantiates {@link SearchTextFinder}
+     * @param prop Expression returning a searchable string for the given component, e.g. {@code JLabel::getText}.
+     * @see #add(ComponentProperty)
+     * @see #addArray(ComponentPropertyArray)
+     */
+    public SearchTextFinder(ComponentProperty<T> prop) {
+        properties.add(prop);
+    }
+
+    /**
+     * Add another property.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning a searchable string for the given component, e.g. {@code JLabel::getText}.
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> add(ComponentProperty<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Add a property returning a string array.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning an array of searchable strings for the given component
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> addArray(ComponentPropertyArray<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Add a property returning a string array.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning an array of searchable strings for the given component
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> addItem(ComponentPropertyItem<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Get the searchable texts for the given component
+     * @param c component
+     * @return {@code List<String>} or an empty list if it's not the right type.
+     */
+    public List<SearchItem> getSearchItems(Component c) {
+        List<SearchItem> ret = new ArrayList<>();
+
+        try {
+            @SuppressWarnings("unchecked")
+            T component = (T) c;
+            if (component != null) {
+                properties.forEach(property -> {
+                    List<SearchItem> items = property.getSearchItems(component);
+                    ret.addAll(items);
+                    /*items.forEach(item -> {
+                        if (item.getText() != null && item.getText().trim().length() > 0) {
+                            String txt = item.getText();
+                            if (txt.indexOf('<') != -1) {
+                                txt = stripHtml(txt);
+                            }
+                            if (txt.indexOf(':') == txt.length() - 1) {
+                                txt = txt.substring(0, item.getText().length() - 1);
+                            }
+                            item.setText(txt);
+                            if (item.getTooltip() != null && item.getTooltip().indexOf('<') != -1) {
+                                item.setTooltip(stripHtml(item.getTooltip()));
+                            }
+                            ret.add(item);
+                        }
+                    });*/
+                });
+            }
+
+        } catch (ClassCastException ex) {
+            // can't use 'instanceof' here, because the type information of T will already be stripped away at runtime
+        }
+        return ret;
+    }
+
+    public final static List<SearchTextFinder<?>> DEFAULT_SEARCH_TEXT_FINDERS = Collections.unmodifiableList(Arrays.asList(
+
+                    new SearchTextFinder<>(JLabel::getText),
+                    new SearchTextFinder<>(JRadioButton::getText),
+                    new SearchTextFinder<>(JCheckBox::getText),
+
+                    new SearchTextFinder<JTable>().addItem(table -> {
+                        List<SearchItem> lst = new ArrayList<>();
+                        TableModel tm = table.getModel();
+                        for (int row = 0; row < tm.getRowCount(); row++) {
+                            for (int col = 0; col < tm.getColumnCount(); col++) {
+                                Object obj = tm.getValueAt(row, col);
+                                Component renderer = table.getCellRenderer(row, col)
+                                        .getTableCellRendererComponent(table, obj, false, false, row, col);
+                                if (renderer instanceof JLabel) {
+                                    JLabel label = (JLabel) renderer;
+                                    boolean isLast = col == tm.getColumnCount() - 1;
+                                    lst.add(new SearchItem(label, label.getText(), label.getToolTipText(), isLast, false));
+                                }
+                            }
+                        }
+                        return lst.toArray(new SearchItem[0]);
+                    }),
+
+                    new SearchTextFinder<JComboBox<Object>>().addItem(combobox -> {
+                        List<SearchItem> lst = new ArrayList<>();
+                        int size = combobox.getItemCount();
+                        JList<Object> listStub = new JList<>();
+                        for (int i = 0; i < size; i++) {
+                            Component renderer = combobox.getRenderer().getListCellRendererComponent(listStub,
+                                    combobox.getItemAt(i), i, false, false);
+                            if (renderer instanceof JLabel) {
+                                JLabel label = (JLabel) renderer;
+                                lst.add(new SearchItem(renderer, label.getText(), label.getToolTipText(), false, false));
+                            }
+                        }
+                        return lst.toArray(new SearchItem[0]);
+                    })));
+
+}
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/package-info.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/package-info.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/package-info.java	(working copy)
@@ -0,0 +1 @@
+package org.openstreetmap.josm.gui.preferences.search;
\ No newline at end of file
Index: src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/preferences/search/SearchTextFinder.java	(working copy)
@@ -0,0 +1,200 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.search;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JRadioButton;
+import javax.swing.JTable;
+import javax.swing.table.TableModel;
+
+/**
+ * Class for obtaining the searchable properties of arbitrary components
+ * @param <T> type of the component
+ * @author Bjoeni
+ */
+public class SearchTextFinder<T extends Component> {
+    List<IComponentProperties<T>> properties = new ArrayList<>();
+
+    @FunctionalInterface
+    private interface IComponentProperties<U extends Component> {
+        Object getFrom(U component);
+
+        default List<SearchItem> getSearchItems(U component) {
+            Object obj = getFrom(component);
+
+            if (obj == null) {
+                return new ArrayList<>();
+            }
+
+            if (obj instanceof String) {
+                return Arrays.asList(new SearchItem((String) obj));
+            }
+
+            if (obj instanceof String[]) {
+                return Arrays.asList((String[]) obj).stream().map(SearchItem::new).collect(Collectors.toList());
+            }
+
+            if (obj instanceof SearchItem[]) {
+                return Arrays.asList((SearchItem[]) obj);
+            }
+
+            throw new IllegalArgumentException();
+        }
+    }
+
+    @FunctionalInterface
+    interface ComponentProperty<U extends Component> extends IComponentProperties<U> {
+        @Override
+        String getFrom(U component);
+    }
+
+    @FunctionalInterface
+    interface ComponentPropertyArray<U extends Component> extends IComponentProperties<U> {
+        @Override
+        String[] getFrom(U component);
+    }
+
+    @FunctionalInterface
+    interface ComponentPropertyItem<U extends Component> extends IComponentProperties<U> {
+        @Override
+        SearchItem[] getFrom(U component);
+    }
+
+    /**
+     * Instantiates {@link SearchTextFinder}
+     */
+    public SearchTextFinder() {
+    }
+
+    /**
+     * Instantiates {@link SearchTextFinder}
+     * @param prop Expression returning a searchable string for the given component, e.g. {@code JLabel::getText}.
+     * @see #add(ComponentProperty)
+     * @see #addArray(ComponentPropertyArray)
+     */
+    public SearchTextFinder(ComponentProperty<T> prop) {
+        properties.add(prop);
+    }
+
+    /**
+     * Add another property.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning a searchable string for the given component, e.g. {@code JLabel::getText}.
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> add(ComponentProperty<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Add a property returning a string array.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning an array of searchable strings for the given component
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> addArray(ComponentPropertyArray<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Add a property returning a string array.
+     * The compiler can't deal with generic vararg parameters properly, so just use this function instead
+     * @param prop Expression returning an array of searchable strings for the given component
+     * @return this (for chaining)
+     */
+    public SearchTextFinder<T> addItem(ComponentPropertyItem<T> prop) {
+        properties.add(prop);
+        return this;
+    }
+
+    /**
+     * Get the searchable texts for the given component
+     * @param c component
+     * @return {@code List<String>} or an empty list if it's not the right type.
+     */
+    public List<SearchItem> getSearchItems(Component c) {
+        List<SearchItem> ret = new ArrayList<>();
+
+        try {
+            @SuppressWarnings("unchecked")
+            T component = (T) c;
+            if (component != null) {
+                properties.forEach(property -> {
+                    List<SearchItem> items = property.getSearchItems(component);
+                    ret.addAll(items);
+                    /*items.forEach(item -> {
+                        if (item.getText() != null && item.getText().trim().length() > 0) {
+                            String txt = item.getText();
+                            if (txt.indexOf('<') != -1) {
+                                txt = stripHtml(txt);
+                            }
+                            if (txt.indexOf(':') == txt.length() - 1) {
+                                txt = txt.substring(0, item.getText().length() - 1);
+                            }
+                            item.setText(txt);
+                            if (item.getTooltip() != null && item.getTooltip().indexOf('<') != -1) {
+                                item.setTooltip(stripHtml(item.getTooltip()));
+                            }
+                            ret.add(item);
+                        }
+                    });*/
+                });
+            }
+
+        } catch (ClassCastException ex) {
+            // can't use 'instanceof' here, because the type information of T will already be stripped away at runtime
+        }
+        return ret;
+    }
+
+    public final static List<SearchTextFinder<?>> DEFAULT_SEARCH_TEXT_FINDERS = Collections.unmodifiableList(Arrays.asList(
+
+                    new SearchTextFinder<>(JLabel::getText),
+                    new SearchTextFinder<>(JRadioButton::getText),
+                    new SearchTextFinder<>(JCheckBox::getText),
+
+                    new SearchTextFinder<JTable>().addItem(table -> {
+                        List<SearchItem> lst = new ArrayList<>();
+                        TableModel tm = table.getModel();
+                        for (int row = 0; row < tm.getRowCount(); row++) {
+                            for (int col = 0; col < tm.getColumnCount(); col++) {
+                                Object obj = tm.getValueAt(row, col);
+                                Component renderer = table.getCellRenderer(row, col)
+                                        .getTableCellRendererComponent(table, obj, false, false, row, col);
+                                if (renderer instanceof JLabel) {
+                                    JLabel label = (JLabel) renderer;
+                                    boolean isLast = col == tm.getColumnCount() - 1;
+                                    lst.add(new SearchItem(label, label.getText(), label.getToolTipText(), isLast, false));
+                                }
+                            }
+                        }
+                        return lst.toArray(new SearchItem[0]);
+                    }),
+
+                    new SearchTextFinder<JComboBox<Object>>().addItem(combobox -> {
+                        List<SearchItem> lst = new ArrayList<>();
+                        int size = combobox.getItemCount();
+                        JList<Object> listStub = new JList<>();
+                        for (int i = 0; i < size; i++) {
+                            Component renderer = combobox.getRenderer().getListCellRendererComponent(listStub,
+                                    combobox.getItemAt(i), i, false, false);
+                            if (renderer instanceof JLabel) {
+                                JLabel label = (JLabel) renderer;
+                                lst.add(new SearchItem(renderer, label.getText(), label.getToolTipText(), false, false));
+                            }
+                        }
+                        return lst.toArray(new SearchItem[0]);
+                    })));
+
+}
\ No newline at end of file
