Index: src/org/openstreetmap/josm/gui/MapMover.java
===================================================================
--- src/org/openstreetmap/josm/gui/MapMover.java	(revision 19434)
+++ src/org/openstreetmap/josm/gui/MapMover.java	(working copy)
@@ -20,6 +20,7 @@
 import org.openstreetmap.josm.actions.mapmode.SelectAction;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.DoubleProperty;
 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -42,6 +43,8 @@
      * Zoom wheel is reversed.
      */
     public static final BooleanProperty PROP_ZOOM_REVERSE_WHEEL = new BooleanProperty("zoom.reverse-wheel", false);
+    public static final BooleanProperty PROP_ZOOM_SMOOTH_SCROLL = new BooleanProperty("zoom.smooth-scroll", false);
+    public static final DoubleProperty PROP_ZOOM_SCROLL_SENSITIVITY = new DoubleProperty("zoom.scroll-sensitivity", 1.0);
 
     static {
         new JMapViewerUpdater();
@@ -253,8 +256,14 @@
      */
     @Override
     public void mouseWheelMoved(MouseWheelEvent e) {
-        int rotation = Boolean.TRUE.equals(PROP_ZOOM_REVERSE_WHEEL.get()) ? -e.getWheelRotation() : e.getWheelRotation();
-        nc.zoomManyTimes(e.getX(), e.getY(), rotation);
+        double scrollAmount = Boolean.TRUE.equals(PROP_ZOOM_SMOOTH_SCROLL.get())
+                ? e.getScrollAmount() * e.getPreciseWheelRotation()
+                : e.getWheelRotation();
+        scrollAmount *= PROP_ZOOM_SCROLL_SENSITIVITY.get();
+        if (Boolean.TRUE.equals(PROP_ZOOM_REVERSE_WHEEL.get())) {
+            scrollAmount *= -1;
+        }
+        nc.zoomManyTimes(e.getX(), e.getY(), scrollAmount);
     }
 
     /**
Index: src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 19434)
+++ src/org/openstreetmap/josm/gui/NavigatableComponent.java	(working copy)
@@ -317,21 +317,23 @@
     /**
      * Get a new scale that is zoomed in/out a number of times
      * from previous scale and snapped to selected native scale layer.
-     * @param times count of zoom operations, negative means zoom in
+     * @param amount how much to zoom, negative means zoom in
      * @return new scale
      */
-    public double scaleZoomManyTimes(int times) {
-        if (nativeScaleLayer != null) {
+    public double scaleZoomManyTimes(double amount) {
+        // Ignore native scale if smooth scrolling
+        if (nativeScaleLayer != null && Boolean.FALSE.equals(MapMover.PROP_ZOOM_SMOOTH_SCROLL.get())) {
             ScaleList scaleList = nativeScaleLayer.getNativeScales();
             if (scaleList != null) {
                 if (Boolean.TRUE.equals(PROP_ZOOM_INTERMEDIATE_STEPS.get())) {
                     scaleList = scaleList.withIntermediateSteps(PROP_ZOOM_RATIO.get());
                 }
-                Scale s = scaleList.scaleZoomTimes(getScale(), PROP_ZOOM_RATIO.get(), times);
+                Scale s = scaleList.scaleZoomTimes(getScale(), PROP_ZOOM_RATIO.get(), (int) Math.ceil(amount));
                 return s != null ? s.getScale() : 0;
             }
         }
-        return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), times);
+        // It turns out with smooth scrolling this factor still feels nice
+        return getScale() * Math.pow(PROP_ZOOM_RATIO.get(), amount);
     }
 
     /**
@@ -363,7 +365,8 @@
      * @return new scale
      */
     public double scaleSnap(double scale, boolean floor) {
-        if (nativeScaleLayer != null) {
+        // Ignore native scale if smooth scrolling
+        if (nativeScaleLayer != null && Boolean.FALSE.equals(MapMover.PROP_ZOOM_SMOOTH_SCROLL.get())) {
             ScaleList scaleList = nativeScaleLayer.getNativeScales();
             if (scaleList != null) {
                 if (Boolean.TRUE.equals(PROP_ZOOM_INTERMEDIATE_STEPS.get())) {
@@ -880,9 +883,9 @@
         }
     }
 
-    public void zoomManyTimes(double x, double y, int times) {
+    public void zoomManyTimes(double x, double y, double amount) {
         double oldScale = getScale();
-        double newScale = scaleZoomManyTimes(times);
+        double newScale = scaleZoomManyTimes(amount);
         zoomToFactor(x, y, newScale / oldScale);
     }
 
Index: src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java
===================================================================
--- src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java	(revision 19434)
+++ src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java	(working copy)
@@ -99,8 +99,10 @@
     private final JCheckBox dialogGeometry = new JCheckBox(tr("Remember dialog geometries"));
     private final JCheckBox nativeFileChoosers = new JCheckBox(tr("Use native file choosers (nicer, but do not support file filters)"));
     private final JCheckBox zoomReverseWheel = new JCheckBox(tr("Reverse zoom with mouse wheel"));
+    private final JCheckBox zoomSmooth = new JCheckBox(tr("Use smooth scroll zooming"));
     private final JCheckBox zoomIntermediateSteps = new JCheckBox(tr("Intermediate steps between native resolutions"));
     private JSpinner spinZoomRatio;
+    private JSpinner spinScrollSensitivity;
 
     @Override
     public void addGui(PreferenceTabbedPane gui) {
@@ -213,6 +215,10 @@
         zoomReverseWheel.setSelected(MapMover.PROP_ZOOM_REVERSE_WHEEL.get());
         panel.add(zoomReverseWheel, GBC.eop().insets(20, 0, 0, 0));
 
+        zoomSmooth.setToolTipText(tr("Check to zoom in by continuous amounts instead of discrete steps"));
+        zoomSmooth.setSelected(MapMover.PROP_ZOOM_SMOOTH_SCROLL.get());
+        panel.add(zoomSmooth, GBC.eop().insets(20, 0, 0, 0));
+
         zoomIntermediateSteps.setToolTipText(
                 tr("Divide intervals between native resolution levels to smaller steps if they are much larger than zoom ratio"));
         zoomIntermediateSteps.setSelected(NavigatableComponent.PROP_ZOOM_INTERMEDIATE_STEPS.get());
@@ -221,21 +227,19 @@
 
         panel.add(Box.createVerticalGlue(), GBC.eol().insets(0, 10, 0, 0));
 
+        double sensitivity = MapMover.PROP_ZOOM_SCROLL_SENSITIVITY.get();
+        spinScrollSensitivity = new JSpinner(new SpinnerNumberModel(sensitivity, 0.01, 2.0, 0.01));
+        var zoomSensitivityToolTipText = tr("Higher values means faster zooming from scrolling");
+        addSpinner(spinScrollSensitivity, tr("Zoom sensitivity"), zoomSensitivityToolTipText, 3);
+
         double logZoomLevel = Math.log(2) / Math.log(NavigatableComponent.PROP_ZOOM_RATIO.get());
         logZoomLevel = Math.max(1, logZoomLevel);
         logZoomLevel = Math.min(5, logZoomLevel);
-        JLabel labelZoomRatio = new JLabel(tr("Zoom steps to get double scale"));
         spinZoomRatio = new JSpinner(new SpinnerNumberModel(logZoomLevel, 1, 10, 1));
-        Component spinZoomRatioEditor = spinZoomRatio.getEditor();
-        JFormattedTextField jftf = ((JSpinner.DefaultEditor) spinZoomRatioEditor).getTextField();
-        jftf.setColumns(2);
-        String zoomRatioToolTipText = tr("Higher value means more steps needed, therefore zoom steps will be smaller");
-        spinZoomRatio.setToolTipText(zoomRatioToolTipText);
-        labelZoomRatio.setToolTipText(zoomRatioToolTipText);
-        labelZoomRatio.setLabelFor(spinZoomRatio);
-        panel.add(labelZoomRatio, GBC.std().insets(20, 0, 0, 0));
-        panel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
-        panel.add(spinZoomRatio, GBC.eol());
+        String labelZoomRatio = tr("Zoom steps to get double scale");
+        String zoomRatioToolTipText = tr("Higher value means more steps needed, therefore zoom steps will be " +
+                "smaller. Also slows down smooth scrolling");
+        addSpinner(spinZoomRatio, labelZoomRatio, zoomRatioToolTipText, 2);
 
         JScrollPane scrollpane = panel.getVerticalScrollPane();
         scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
@@ -242,6 +246,18 @@
         gui.getDisplayPreference().addSubTab(this, tr("Look and Feel"), scrollpane);
     }
 
+    private void addSpinner(JSpinner spinner, String labelText, String tooltip, int columns) {
+        ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(columns);
+        var label = new JLabel(labelText);
+        spinner.setToolTipText(tooltip);
+        label.setToolTipText(tooltip);
+        label.setLabelFor(spinner);
+
+        panel.add(label, GBC.std().insets(20, 0, 0, 0));
+        panel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL));
+        panel.add(spinner, GBC.eol());
+    }
+
     @Override
     public boolean ok() {
         boolean mod = false;
@@ -259,8 +275,10 @@
         WindowGeometry.GUI_GEOMETRY_ENABLED.put(dialogGeometry.isSelected());
         Config.getPref().putBoolean(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.getKey(), nativeFileChoosers.isSelected());
         MapMover.PROP_ZOOM_REVERSE_WHEEL.put(zoomReverseWheel.isSelected());
+        MapMover.PROP_ZOOM_SMOOTH_SCROLL.put(zoomSmooth.isSelected());
         NavigatableComponent.PROP_ZOOM_INTERMEDIATE_STEPS.put(zoomIntermediateSteps.isSelected());
         NavigatableComponent.PROP_ZOOM_RATIO.put(Math.pow(2, 1/(double) spinZoomRatio.getModel().getValue()));
+        MapMover.PROP_ZOOM_SCROLL_SENSITIVITY.put((double) spinScrollSensitivity.getValue());
         mod |= LAF.put(((LookAndFeelInfo) lafCombo.getSelectedItem()).getClassName());
         return mod;
     }
