diff --git a/novelwriter/extensions/wheeleventfilter.py b/novelwriter/extensions/wheeleventfilter.py
new file mode 100644
index 00000000..220fa324
--- /dev/null
+++ b/novelwriter/extensions/wheeleventfilter.py
@@ -0,0 +1,65 @@
+"""
+novelWriter – Custom Object: Wheel Event Filter
+===============================================
+
+File History:
+Created: 2023-08-31 [2.1rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2023, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+from PyQt5.QtGui import QWheelEvent
+from PyQt5.QtCore import QEvent, QObject
+from PyQt5.QtWidgets import QWidget
+
+
+class WheelEventFilter(QObject):
+ """Extensions: Wheel Event Filter
+
+ An event filter that filters mouse wheel events for a widget and
+ forward them to the root widget. This solves the lack of mouse wheel
+ scrolling response in margins of widgets like the editor and viewer.
+
+ Solves: https://github.com/vkbo/novelWriter/issues/1425
+ Reference: https://stackoverflow.com/a/17739995/5825851
+ """
+
+ __slots__ = ("_parent", "_locked")
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+ self._parent = parent
+ self._locked = False
+ return
+
+ def eventFilter(self, object: QObject, event: QEvent) -> bool:
+ """Filter events of type QWheelEvent and forward them to the
+ parent widget's wheelEvent handler.
+ """
+ if self._locked:
+ return False
+ if isinstance(event, QWheelEvent):
+ # Recursion protection may be redundant, but a comment on
+ # StackOverflow points to Qt 5.12 as the change, and we
+ # still support Qt 5.10
+ self._locked = True
+ self._parent.wheelEvent(event)
+ self._locked = False
+ return False
+
+# END Class WheelEventFilter
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 5d971ca6..c93db0a8 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -55,6 +55,7 @@ from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.core.index import countWords
from novelwriter.gui.dochighlight import GuiDocHighlighter
+from novelwriter.extensions.wheeleventfilter import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -178,6 +179,10 @@ class GuiDocEditor(QTextEdit):
self.wCounterSel.setAutoDelete(False)
self.wCounterSel.signals.countsReady.connect(self._updateSelCounts)
+ # Install Event Filter for Mouse Wheel
+ self.wheelEventFilter = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventFilter)
+
# Finalise
self.updateSyntaxColours()
self.initEditor()
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index bbfb4c64..ca835ea6 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -47,6 +47,7 @@ from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
from novelwriter.constants import nwUnicode
from novelwriter.core.tohtml import ToHtml
+from novelwriter.extensions.wheeleventfilter import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -85,6 +86,10 @@ class GuiDocViewer(QTextBrowser):
# Signals
self.anchorClicked.connect(self._linkClicked)
+ # Install Event Filter for Mouse Wheel
+ self.wheelEventFilter = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventFilter)
+
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 02b70150..2a186a75 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -3,7 +3,7 @@ novelWriter – Shared Data Class
===============================
File History:
-Created: 2023-08-10 [2.1b2]
+Created: 2023-08-10 [2.1rc1]
This file is a part of novelWriter
Copyright 2018–2023, Veronica Berglyd Olsen
diff --git a/tests/test_ext/test_ext_wheeleventfilter.py b/tests/test_ext/test_ext_wheeleventfilter.py
new file mode 100644
index 00000000..1120a237
--- /dev/null
+++ b/tests/test_ext/test_ext_wheeleventfilter.py
@@ -0,0 +1,67 @@
+"""
+novelWriter – Wheel Event Filter Tester
+=======================================
+
+This file is a part of novelWriter
+Copyright 2018–2023, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+from PyQt5.QtCore import QEvent, QObject, QPoint, Qt
+import pytest
+
+from PyQt5.QtGui import QKeyEvent, QWheelEvent
+from PyQt5.QtWidgets import QWidget
+
+from novelwriter.extensions.wheeleventfilter import WheelEventFilter
+
+
+class MockWidget(QWidget):
+
+ def __init__(self):
+ super().__init__(None)
+ self.count = 0
+ return
+
+ def wheelEvent(self, event: QWheelEvent) -> None:
+ self.count += 1
+ return
+
+
+@pytest.mark.gui
+def testExtWheelEventFilter_Main():
+ """Test the WheelEventFilter class."""
+ obj = QObject()
+ widget = MockWidget()
+ eFilter = WheelEventFilter(widget)
+ assert widget.count == 0
+
+ # Sending a key event does nothing
+ event = QKeyEvent(QEvent.KeyPress, 1, Qt.ShiftModifier)
+ eFilter.eventFilter(obj, event)
+ assert widget.count == 0
+
+ # Sending a mouse wheel event forwards it
+ pos = QPoint(0, 0)
+ event = QWheelEvent(pos, pos, pos, pos, Qt.NoButton, Qt.NoModifier, Qt.NoScrollPhase, False)
+ eFilter.eventFilter(obj, event)
+ assert widget.count == 1
+
+ # But ignore if we're locked
+ eFilter._locked = True
+ eFilter.eventFilter(obj, event)
+ assert widget.count == 1
+
+# END Test testExtWheelEventFilter_Main