From 94329b334acd14cf0839113c88676157bcec7982 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 31 Aug 2023 11:40:52 +0200
Subject: [PATCH 1/2] Forward all scroll events in editor/viewer areas to their
text area widgets (#1425)
---
novelwriter/extensions/wheeleventfilter.py | 65 ++++++++++++++++++++++
novelwriter/gui/doceditor.py | 5 ++
novelwriter/gui/docviewer.py | 5 ++
3 files changed, 75 insertions(+)
create mode 100644 novelwriter/extensions/wheeleventfilter.py
diff --git a/novelwriter/extensions/wheeleventfilter.py b/novelwriter/extensions/wheeleventfilter.py
new file mode 100644
index 00000000..75340cfa
--- /dev/null
+++ b/novelwriter/extensions/wheeleventfilter.py
@@ -0,0 +1,65 @@
+"""
+novelWriter – Custom Object: Wheel Event Filter
+===============================================
+
+File History:
+Created: 2023-08-31 [2.1b2]
+
+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..c10765aa 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.wheelEventForwarder = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventForwarder)
+
# Finalise
self.updateSyntaxColours()
self.initEditor()
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index bbfb4c64..91992c00 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.wheelEventForwarder = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventForwarder)
+
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
From d7c8f97d47c24f5c89e1373a82555266c68110e2 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 31 Aug 2023 16:38:51 +0200
Subject: [PATCH 2/2] Add test coverage and fix a few minor things
---
novelwriter/extensions/wheeleventfilter.py | 2 +-
novelwriter/gui/doceditor.py | 4 +-
novelwriter/gui/docviewer.py | 4 +-
novelwriter/shared.py | 2 +-
tests/test_ext/test_ext_wheeleventfilter.py | 67 +++++++++++++++++++++
5 files changed, 73 insertions(+), 6 deletions(-)
create mode 100644 tests/test_ext/test_ext_wheeleventfilter.py
diff --git a/novelwriter/extensions/wheeleventfilter.py b/novelwriter/extensions/wheeleventfilter.py
index 75340cfa..220fa324 100644
--- a/novelwriter/extensions/wheeleventfilter.py
+++ b/novelwriter/extensions/wheeleventfilter.py
@@ -3,7 +3,7 @@ novelWriter – Custom Object: Wheel Event Filter
===============================================
File History:
-Created: 2023-08-31 [2.1b2]
+Created: 2023-08-31 [2.1rc1]
This file is a part of novelWriter
Copyright 2018–2023, Veronica Berglyd Olsen
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index c10765aa..c93db0a8 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -180,8 +180,8 @@ class GuiDocEditor(QTextEdit):
self.wCounterSel.signals.countsReady.connect(self._updateSelCounts)
# Install Event Filter for Mouse Wheel
- self.wheelEventForwarder = WheelEventFilter(self)
- self.installEventFilter(self.wheelEventForwarder)
+ self.wheelEventFilter = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventFilter)
# Finalise
self.updateSyntaxColours()
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 91992c00..ca835ea6 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -87,8 +87,8 @@ class GuiDocViewer(QTextBrowser):
self.anchorClicked.connect(self._linkClicked)
# Install Event Filter for Mouse Wheel
- self.wheelEventForwarder = WheelEventFilter(self)
- self.installEventFilter(self.wheelEventForwarder)
+ self.wheelEventFilter = WheelEventFilter(self)
+ self.installEventFilter(self.wheelEventFilter)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
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