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