Index: trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 18573)
+++ trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 18574)
@@ -9,4 +9,6 @@
 import java.awt.event.HierarchyEvent;
 import java.awt.event.HierarchyListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
@@ -21,4 +23,5 @@
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Set;
 import java.util.Stack;
@@ -41,4 +44,5 @@
 import org.openstreetmap.josm.data.osm.BBox;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -53,4 +57,5 @@
 import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
+import org.openstreetmap.josm.gui.PrimitiveHoverListener.PrimitiveHoverEvent;
 import org.openstreetmap.josm.gui.help.Helpful;
 import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
@@ -64,4 +69,5 @@
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
+import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler;
 
 /**
@@ -150,4 +156,55 @@
     }
 
+    private final CopyOnWriteArrayList<PrimitiveHoverListener> primitiveHoverListeners = new CopyOnWriteArrayList<>();
+    private IPrimitive previousHoveredPrimitive;
+    private final PrimitiveHoverMouseListener primitiveHoverMouseListenerHelper = new PrimitiveHoverMouseListener();
+
+    /**
+     * Removes a primitive hover listener
+     *
+     * @param listener the listener. Ignored if null or already absent.
+     * @since 18574
+     */
+    public void removePrimitiveHoverListener(PrimitiveHoverListener listener) {
+        primitiveHoverListeners.remove(listener);
+    }
+
+    /**
+     * Adds a primitive hover listener
+     *
+     * @param listener the listener. Ignored if null or already registered.
+     * @since 18574
+     */
+    public void addPrimitiveHoverListener(PrimitiveHoverListener listener) {
+        if (listener != null) {
+            primitiveHoverListeners.addIfAbsent(listener);
+        }
+    }
+
+    /**
+     * Send a {@link PrimitiveHoverEvent} to registered {@link PrimitiveHoverListener}s
+     * @param e primitive hover event information
+     * @since 18574
+     */
+    protected void firePrimitiveHovered(PrimitiveHoverEvent e) {
+        GuiHelper.runInEDT(() -> {
+            for (PrimitiveHoverListener l : primitiveHoverListeners) {
+                try {
+                    l.primitiveHovered(e);
+                } catch (RuntimeException ex) {
+                    Logging.logWithStackTrace(Logging.LEVEL_ERROR, "Error in primitive hover listener", ex);
+                    BugReportExceptionHandler.handleException(ex);
+                }
+            }
+        });
+    }
+
+    private void updateHoveredPrimitive(IPrimitive hovered, MouseEvent e) {
+        if (!Objects.equals(hovered, previousHoveredPrimitive)) {
+            firePrimitiveHovered(new PrimitiveHoverEvent(hovered, previousHoveredPrimitive, e));
+            previousHoveredPrimitive = hovered;
+        }
+    }
+
     // The only events that may move/resize this map view are window movements or changes to the map view size.
     // We can clean this up more by only recalculating the state on repaint.
@@ -199,4 +256,5 @@
         addHierarchyListener(hierarchyListener);
         addComponentListener(componentListener);
+        addPrimitiveHoverMouseListeners();
         super.addNotify();
     }
@@ -206,5 +264,16 @@
         removeHierarchyListener(hierarchyListener);
         removeComponentListener(componentListener);
+        removePrimitiveHoverMouseListeners();
         super.removeNotify();
+    }
+
+    private void addPrimitiveHoverMouseListeners() {
+        addMouseMotionListener(primitiveHoverMouseListenerHelper);
+        addMouseListener(primitiveHoverMouseListenerHelper);
+    }
+
+    private void removePrimitiveHoverMouseListeners() {
+        removeMouseMotionListener(primitiveHoverMouseListenerHelper);
+        removeMouseListener(primitiveHoverMouseListenerHelper);
     }
 
@@ -1716,3 +1785,20 @@
         )/512;
     }
+
+    /**
+     * Listener for mouse movement events. Used to detect when primitives are being hovered over with the mouse pointer
+     * so that registered {@link PrimitiveHoverListener}s can be notified.
+     */
+    private class PrimitiveHoverMouseListener extends MouseAdapter {
+        @Override
+        public void mouseMoved(MouseEvent e) {
+            OsmPrimitive hovered = getNearestNodeOrWay(e.getPoint(), isSelectablePredicate, true);
+            updateHoveredPrimitive(hovered, e);
+        }
+
+        @Override
+        public void mouseExited(MouseEvent e) {
+            updateHoveredPrimitive(null, e);
+        }
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/PrimitiveHoverListener.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/PrimitiveHoverListener.java	(revision 18574)
+++ trunk/src/org/openstreetmap/josm/gui/PrimitiveHoverListener.java	(revision 18574)
@@ -0,0 +1,68 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui;
+
+import java.awt.event.MouseEvent;
+
+import org.openstreetmap.josm.data.osm.IPrimitive;
+
+/**
+ * Interface to notify listeners when the user moves the mouse pointer onto or off of a primitive.
+ * @since 18574
+ */
+@FunctionalInterface
+public interface PrimitiveHoverListener {
+    /**
+     * Method called when the primitive under the mouse pointer changes.
+     * @param e Event object describing the hovered primitive and related information
+     */
+    void primitiveHovered(PrimitiveHoverEvent e);
+
+    /**
+     * Event that is fired when the mouse pointer is moved over a primitive.
+     */
+    class PrimitiveHoverEvent {
+        /**
+         * The primitive that is being hovered over by the mouse pointer.
+         * Can be null if the mouse pointer is not over any primitive.
+         */
+        private final IPrimitive hoveredPrimitive;
+        private final IPrimitive previousPrimitive;
+        private final MouseEvent mouseEvent;
+
+        /**
+         * Construct a new {@code PrimitiveHoverEvent}
+         * @param hoveredPrimitive Primitive that is hovered by the mouse pointer
+         * @param previousPrimitive Previously hovered primitive
+         * @param mouseEvent {@link MouseEvent} that triggered this hover event
+         */
+        public PrimitiveHoverEvent(IPrimitive hoveredPrimitive, IPrimitive previousPrimitive, MouseEvent mouseEvent) {
+            this.hoveredPrimitive = hoveredPrimitive;
+            this.previousPrimitive = previousPrimitive;
+            this.mouseEvent = mouseEvent;
+        }
+
+        /**
+         * Get the primitive that is being hovered over with the mouse pointer
+         * @return The primitive that is being hovered over
+         */
+        public IPrimitive getHoveredPrimitive() {
+            return hoveredPrimitive;
+        }
+
+        /**
+         * Get the previously hovered primitive
+         * @return The previously hovered primitive
+         */
+        public IPrimitive getPreviousPrimitive() {
+            return previousPrimitive;
+        }
+
+        /**
+         * Get the {@link MouseEvent} object that triggered this hover event
+         * @return The {@link MouseEvent} that triggered this hover event
+         */
+        public MouseEvent getMouseEvent() {
+            return mouseEvent;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 18573)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 18574)
@@ -13,4 +13,6 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -69,5 +71,4 @@
 import org.openstreetmap.josm.data.osm.KeyValueVisitor;
 import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmDataManager;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -84,9 +85,13 @@
 import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.search.SearchSetting;
+import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent;
+import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.CachingProperty;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.PopupMenuHandler;
+import org.openstreetmap.josm.gui.PrimitiveHoverListener;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
@@ -97,4 +102,5 @@
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
+import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
@@ -141,5 +147,6 @@
  */
 public class PropertiesDialog extends ToggleDialog
-implements DataSelectionListener, ActiveLayerChangeListener, DataSetListenerAdapter.Listener, TaggingPresetListener {
+implements DataSelectionListener, ActiveLayerChangeListener, PropertyChangeListener, 
+        DataSetListenerAdapter.Listener, TaggingPresetListener, PrimitiveHoverListener {
     private final BooleanProperty PROP_DISPLAY_DISCARDABLE_KEYS = new BooleanProperty("display.discardable-keys", false);
 
@@ -249,4 +256,18 @@
 
     /**
+     * Show tags and relation memberships of objects in the properties dialog when hovering over them with the mouse pointer
+     * @since 18574
+     */
+    public static final BooleanProperty PROP_PREVIEW_ON_HOVER = new BooleanProperty("propertiesdialog.preview-on-hover", true);
+    private final HoverPreviewPropListener hoverPreviewPropListener = new HoverPreviewPropListener();
+
+    /**
+     * Always show information for selected objects when something is selected instead of the hovered object
+     * @since 18574
+     */
+    public static final CachingProperty<Boolean> PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION = 
+        new BooleanProperty("propertiesdialog.preview-on-hover.always-show-selected", true).cached();
+
+    /**
      * Create a new PropertiesDialog
      */
@@ -305,4 +326,6 @@
 
         TaggingPresets.addListener(this);
+
+        PROP_PREVIEW_ON_HOVER.addListener(hoverPreviewPropListener);
     }
 
@@ -587,4 +610,6 @@
         SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
         MainApplication.getLayerManager().addActiveLayerChangeListener(this);
+        if (PROP_PREVIEW_ON_HOVER.get())
+            MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
         for (JosmAction action : josmActions) {
             MainApplication.registerActionShortcut(action);
@@ -598,4 +623,5 @@
         SelectionEventManager.getInstance().removeSelectionListener(this);
         MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
+        MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
         for (JosmAction action : josmActions) {
             MainApplication.unregisterActionShortcut(action);
@@ -618,4 +644,5 @@
         super.destroy();
         TaggingPresets.removeListener(this);
+        PROP_PREVIEW_ON_HOVER.removeListener(hoverPreviewPropListener);
         Container parent = pluginHook.getParent();
         if (parent != null) {
@@ -636,5 +663,43 @@
         // Ignore parameter as we do not want to operate always on real selection here, especially in draw mode
         Collection<? extends IPrimitive> newSel = OsmDataManager.getInstance().getInProgressISelection();
-        int newSelSize = newSel.size();
+
+        // Temporarily disable listening to primitive mouse hover events while we have a selection as that takes priority
+        if (PROP_PREVIEW_ON_HOVER.get() && PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get()) {
+            if (newSel.isEmpty()) {
+                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
+            } else {
+                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
+            }
+        }
+
+        updateUi(newSel);
+    }
+
+    @Override
+    public void primitiveHovered(PrimitiveHoverEvent e) {
+        Collection<? extends IPrimitive> selection = OsmDataManager.getInstance().getInProgressISelection();
+        if (PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get() && !selection.isEmpty())
+            return;
+
+        if (e.getHoveredPrimitive() != null) {
+            updateUi(e.getHoveredPrimitive());
+        } else {
+            updateUi(selection);
+        }
+    }
+
+    private void autoresizeTagTable() {
+        if (PROP_AUTORESIZE_TAGS_TABLE.get()) {
+            // resize table's columns to fit content
+            TableHelper.computeColumnsWidth(tagTable);
+        }
+    }
+
+    private void updateUi(IPrimitive primitive) {
+        updateUi(primitive == null ? Collections.emptyList() :
+                                     Collections.singletonList(primitive));
+    }
+
+    private void updateUi(Collection<? extends IPrimitive> primitives) {
         IRelation<?> selectedRelation = null;
         String selectedTag = editHelper.getChangedKey(); // select last added or last edited key by default
@@ -645,4 +710,31 @@
             selectedRelation = (IRelation<?>) membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
         }
+
+        updateTagTableData(primitives);
+        updateMembershipTableData(primitives);
+
+        updateMembershipTableVisibility();
+        updateActionsEnabledState();
+        updateTagTableVisibility(primitives);
+
+        setupTaginfoNationalActions(primitives);
+        autoresizeTagTable();
+
+        int selectedIndex;
+        if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
+            tagTable.changeSelection(selectedIndex, 0, false, false);
+        } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
+            membershipTable.changeSelection(selectedIndex, 0, false, false);
+        } else if (tagData.getRowCount() > 0) {
+            tagTable.changeSelection(0, 0, false, false);
+        } else if (membershipData.getRowCount() > 0) {
+            membershipTable.changeSelection(0, 0, false, false);
+        }
+
+        updateTitle(primitives);
+    }
+
+    private void updateTagTableData(Collection<? extends IPrimitive> primitives) {
+        int newSelSize = primitives.size();
 
         // re-load tag data
@@ -654,5 +746,5 @@
         valueCount.clear();
         Set<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
-        for (IPrimitive osm : newSel) {
+        for (IPrimitive osm : primitives) {
             types.add(TaggingPresetType.forPrimitive(osm));
             osm.visitKeys((p, key, value) -> {
@@ -680,12 +772,16 @@
         }
 
+        presets.updatePresets(types, tags, presetHandler);
+    }
+
+    private void updateMembershipTableData(Collection<? extends IPrimitive> primitives) {
         membershipData.setRowCount(0);
 
         Map<IRelation<?>, MemberInfo> roles = new HashMap<>();
-        for (IPrimitive primitive: newSel) {
-            for (IPrimitive ref: primitive.getReferrers(true)) {
+        for (IPrimitive primitive : primitives) {
+            for (IPrimitive ref : primitive.getReferrers(true)) {
                 if (ref instanceof IRelation && !ref.isIncomplete() && !ref.isDeleted()) {
                     IRelation<?> r = (IRelation<?>) ref;
-                    MemberInfo mi = roles.computeIfAbsent(r, ignore -> new MemberInfo(newSel));
+                    MemberInfo mi = roles.computeIfAbsent(r, ignore -> new MemberInfo(primitives));
                     int i = 1;
                     for (IRelationMember<?> m : r.getMembers()) {
@@ -708,18 +804,15 @@
             membershipData.addRow(new Object[]{r, roles.get(r)});
         }
-
-        presets.updatePresets(types, tags, presetHandler);
-
+    }
+
+    private void updateMembershipTableVisibility() {
         membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
         membershipTable.setVisible(membershipData.getRowCount() > 0);
-
-        OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
-        boolean isReadOnly = ds != null && ds.isLocked();
-        boolean hasSelection = !newSel.isEmpty();
+    }
+
+    private void updateTagTableVisibility(Collection<? extends IPrimitive> primitives) {
+        boolean hasSelection = !primitives.isEmpty();
         boolean hasTags = hasSelection && tagData.getRowCount() > 0;
-        boolean hasMemberships = hasSelection && membershipData.getRowCount() > 0;
-        addAction.setEnabled(!isReadOnly && hasSelection);
-        editAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
-        deleteAction.setEnabled(!isReadOnly && (hasTags || hasMemberships));
+
         tagTable.setVisible(hasTags);
         tagTable.getTableHeader().setVisible(hasTags);
@@ -727,19 +820,14 @@
         selectSth.setVisible(!hasSelection);
         pluginHook.setVisible(hasSelection);
-
-        setupTaginfoNationalActions(newSel);
-        autoresizeTagTable();
-
-        int selectedIndex;
-        if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
-            tagTable.changeSelection(selectedIndex, 0, false, false);
-        } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
-            membershipTable.changeSelection(selectedIndex, 0, false, false);
-        } else if (hasTags) {
-            tagTable.changeSelection(0, 0, false, false);
-        } else if (hasMemberships) {
-            membershipTable.changeSelection(0, 0, false, false);
-        }
-
+    }
+
+    private void updateActionsEnabledState() {
+        addAction.updateEnabledState();
+        editAction.updateEnabledState();
+        deleteAction.updateEnabledState();
+    }
+
+    private void updateTitle(Collection<? extends IPrimitive> primitives) {
+        int newSelSize = primitives.size();
         if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
             if (newSelSize > 1) {
@@ -752,11 +840,4 @@
         } else {
             setTitle(tr("Tags/Memberships"));
-        }
-    }
-
-    private void autoresizeTagTable() {
-        if (PROP_AUTORESIZE_TAGS_TABLE.get()) {
-            // resize table's columns to fit content
-            TableHelper.computeColumnsWidth(tagTable);
         }
     }
@@ -804,4 +885,36 @@
         // it is time to save history of tags
         updateSelection();
+
+        // Listen for active layer visibility change to enable/disable hover preview
+        // Remove previous listener first (order matters if we are somehow getting a layer change event 
+        // switching from one layer to the same layer)
+        Layer prevLayer = e.getPreviousDataLayer();
+        if (prevLayer != null) {
+            prevLayer.removePropertyChangeListener(this);
+        }
+
+        Layer newLayer = e.getSource().getActiveDataLayer();
+        if (newLayer != null) {
+            newLayer.addPropertyChangeListener(this);
+            if (newLayer.isVisible()) {
+                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
+            } else {
+                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent e) {
+        if (Layer.VISIBLE_PROP.equals(e.getPropertyName())) {
+            boolean isVisible = (boolean) e.getNewValue();
+
+            // Disable hover preview when primitives are invisible
+            if (isVisible) {
+                MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
+            } else {
+                MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
+            }
+        }
     }
 
@@ -1252,4 +1365,11 @@
             }
         }
+
+        @Override
+        protected final void updateEnabledState() {
+            DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
+            setEnabled(ds != null && !ds.isLocked() &&
+                    !Utils.isEmpty(OsmDataManager.getInstance().getInProgressSelection()));
+        }
     }
 
@@ -1405,3 +1525,14 @@
         }
     }
+
+    private class HoverPreviewPropListener implements ValueChangeListener<Boolean> {
+        @Override
+        public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
+            if (e.getProperty().get() && isDialogShowing()) {
+                MainApplication.getMap().mapView.addPrimitiveHoverListener(PropertiesDialog.this);
+            } else if (!e.getProperty().get()) {
+                MainApplication.getMap().mapView.removePrimitiveHoverListener(PropertiesDialog.this);
+            }
+        }
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java	(revision 18573)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java	(revision 18574)
@@ -33,4 +33,5 @@
 import org.openstreetmap.josm.gui.NavigatableComponent;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
+import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog;
 
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
@@ -92,4 +93,6 @@
     private final JCheckBox showLocalizedName = new JCheckBox(tr("Show localized name in selection lists"));
     private final JCheckBox modeless = new JCheckBox(tr("Modeless working (Potlatch style)"));
+    private final JCheckBox previewPropsOnHover = new JCheckBox(tr("Preview object properties on mouse hover"));
+    private final JCheckBox previewPrioritizeSelection = new JCheckBox(tr("Prefer showing information for selected objects"));
     private final JCheckBox dynamicButtons = new JCheckBox(tr("Dynamic buttons in side menus"));
     private final JCheckBox isoDates = new JCheckBox(tr("Display ISO dates"));
@@ -174,4 +177,15 @@
         panel.add(showLocalizedName, GBC.eop().insets(20, 0, 0, 0));
         panel.add(modeless, GBC.eop().insets(20, 0, 0, 0));
+
+        previewPropsOnHover.setToolTipText(
+                tr("Show tags and relation memberships of objects in the properties dialog when hovering over them with the mouse pointer"));
+        previewPropsOnHover.setSelected(PropertiesDialog.PROP_PREVIEW_ON_HOVER.get());
+        panel.add(previewPropsOnHover, GBC.eop().insets(20, 0, 0, 0));
+
+        previewPrioritizeSelection.setToolTipText(
+            tr("Always show information for selected objects when something is selected instead of the hovered object"));
+        previewPrioritizeSelection.setSelected(PropertiesDialog.PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.get());
+        panel.add(previewPrioritizeSelection, GBC.eop().insets(40, 0, 0, 0));
+        previewPropsOnHover.addActionListener(l -> previewPrioritizeSelection.setEnabled(previewPropsOnHover.isSelected()));
 
         dynamicButtons.setToolTipText(tr("Display buttons in right side menus only when mouse is inside the element"));
@@ -239,4 +253,6 @@
         Config.getPref().putBoolean("osm-primitives.localize-name", showLocalizedName.isSelected());
         MapFrame.MODELESS.put(modeless.isSelected());
+        PropertiesDialog.PROP_PREVIEW_ON_HOVER.put(previewPropsOnHover.isSelected());
+        PropertiesDialog.PROP_PREVIEW_ON_HOVER_PRIORITIZE_SELECTION.put(previewPrioritizeSelection.isSelected());
         Config.getPref().putBoolean(ToggleDialog.PROP_DYNAMIC_BUTTONS.getKey(), dynamicButtons.isSelected());
         Config.getPref().putBoolean(DateUtils.PROP_ISO_DATES.getKey(), isoDates.isSelected());
Index: trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 18573)
+++ trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 18574)
@@ -468,5 +468,11 @@
      */
     public boolean isShowable() {
-        return data.stream().anyMatch(i -> !(i instanceof Optional || i instanceof Space || i instanceof Key));
+        // Not using streams makes this method effectively allocation free and uses ~40% fewer CPU cycles.
+        for (TaggingPresetItem i : data) {
+            if (!(i instanceof Optional || i instanceof Space || i instanceof Key)) {
+                return true;
+            }
+        }
+        return false;
     }
 
Index: trunk/test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java	(revision 18573)
+++ trunk/test/unit/org/openstreetmap/josm/gui/NavigatableComponentTest.java	(revision 18574)
@@ -4,9 +4,14 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 
 import java.awt.Point;
 import java.awt.Rectangle;
+import java.awt.event.MouseEvent;
 import java.awt.geom.Point2D;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.swing.JPanel;
@@ -22,5 +27,8 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
@@ -45,9 +53,14 @@
             return true;
         }
+
+        @Override
+        public void processMouseMotionEvent(MouseEvent mouseEvent) {
+            super.processMouseMotionEvent(mouseEvent);
+        }
     }
 
     private static final int HEIGHT = 200;
     private static final int WIDTH = 300;
-    private NavigatableComponent component;
+    private NavigatableComponentMock component;
 
     /**
@@ -66,9 +79,5 @@
         component.setBounds(new Rectangle(WIDTH, HEIGHT));
         // wait for the event to be propagated.
-        GuiHelper.runInEDTAndWait(new Runnable() {
-            @Override
-            public void run() {
-            }
-        });
+        GuiHelper.runInEDTAndWait(() -> { /* Do nothing */ });
         component.setVisible(true);
         JPanel parent = new JPanel();
@@ -207,4 +216,49 @@
         assertThat(bounds.getMin(), CustomMatchers.is(component.getLatLon(0, HEIGHT)));
         assertThat(bounds.getMax(), CustomMatchers.is(component.getLatLon(WIDTH, 0)));
+    }
+
+    @Test
+    void testHoverListeners() {
+        AtomicReference<PrimitiveHoverListener.PrimitiveHoverEvent> hoverEvent = new AtomicReference<>();
+        PrimitiveHoverListener testListener = hoverEvent::set;
+        assertNull(hoverEvent.get());
+        component.addNotify();
+        component.addPrimitiveHoverListener(testListener);
+        DataSet ds = new DataSet();
+        MainApplication.getLayerManager().addLayer(new OsmDataLayer(ds, "testHoverListeners", null));
+        LatLon center = component.getRealBounds().getCenter();
+        Node node1 = new Node(center);
+        ds.addPrimitive(node1);
+        double x = component.getBounds().getCenterX();
+        double y = component.getBounds().getCenterY();
+        // Check hover over primitive
+        MouseEvent node1Event = new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(),
+                0, (int) x, (int) y, 0, false, MouseEvent.NOBUTTON);
+        component.processMouseMotionEvent(node1Event);
+        GuiHelper.runInEDTAndWait(() -> { /* Sync */ });
+        PrimitiveHoverListener.PrimitiveHoverEvent event = hoverEvent.getAndSet(null);
+        assertNotNull(event);
+        assertSame(node1, event.getHoveredPrimitive());
+        assertNull(event.getPreviousPrimitive());
+        assertSame(node1Event, event.getMouseEvent());
+        // Check moving to the (same) primitive. No new mouse motion event should be called.
+        component.processMouseMotionEvent(node1Event);
+        GuiHelper.runInEDTAndWait(() -> { /* Sync */ });
+        event = hoverEvent.getAndSet(null);
+        assertNull(event);
+        // Check moving off primitive. A new mouse motion event should be called with the previous primitive and null.
+        MouseEvent noNodeEvent =
+                new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 0, 0, 0, false, MouseEvent.NOBUTTON);
+        component.processMouseMotionEvent(noNodeEvent);
+        GuiHelper.runInEDTAndWait(() -> { /* Sync */ });
+        event = hoverEvent.getAndSet(null);
+        assertNotNull(event);
+        assertSame(node1, event.getPreviousPrimitive());
+        assertNull(event.getHoveredPrimitive());
+        assertSame(noNodeEvent, event.getMouseEvent());
+        // Check moving to area with no primitive with no previous hover primitive
+        component.processMouseMotionEvent(
+                new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 1, 1, 0, false, MouseEvent.NOBUTTON));
+        assertNull(hoverEvent.get());
     }
 
