Clean up alert boxes and update event filters (#1631)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-29 16:12:08 +01:00
committed by GitHub
19 changed files with 116 additions and 156 deletions
-13
View File
@@ -38,7 +38,6 @@ from urllib.request import pathname2url
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QCoreApplication, QUrl from PyQt5.QtCore import QCoreApplication, QUrl
from PyQt5.QtWidgets import QWidget, qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
@@ -508,18 +507,6 @@ def openExternalPath(path: Path) -> bool:
return False return False
# =============================================================================================== #
# Other Functions
# =============================================================================================== #
def getGuiItem(objName: str) -> QWidget | None:
"""Returns a QtWidget based on its objectName."""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == objName:
return qWidget
return None
# =============================================================================================== # # =============================================================================================== #
# Classes # Classes
# =============================================================================================== # # =============================================================================================== #
+1 -1
View File
@@ -80,7 +80,7 @@ class NWProject:
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWProject") logger.debug("Delete: NWProject")
return return
+1 -1
View File
@@ -56,7 +56,7 @@ class NWSpellEnchant:
logger.debug("Ready: NWSpellEnchant") logger.debug("Ready: NWSpellEnchant")
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWSpellEnchant") logger.debug("Delete: NWSpellEnchant")
return return
@@ -1,9 +1,10 @@
""" """
novelWriter Custom Object: Wheel Event Filter novelWriter Custom Objects: Event Filters
=============================================== ===========================================
File History: File History:
Created: 2023-08-31 [2.1rc1] Created: 2023-08-31 [2.1rc1] WheelEventFilter
Created: 2023-11-28 [2.2] StatusTipFilter
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen Copyright 20182023, Veronica Berglyd Olsen
@@ -23,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
from PyQt5.QtGui import QWheelEvent from PyQt5.QtGui import QStatusTipEvent, QWheelEvent
from PyQt5.QtCore import QEvent, QObject from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
@@ -63,3 +64,12 @@ class WheelEventFilter(QObject):
return False return False
# END Class WheelEventFilter # END Class WheelEventFilter
class StatusTipFilter(QObject):
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Filter out status tip events on menus."""
return True if isinstance(event, QStatusTipEvent) else super().eventFilter(obj, event)
# END Class StatusTipFilter
+1 -1
View File
@@ -62,7 +62,7 @@ from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.core.document import NWDocument from novelwriter.core.document import NWDocument
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.editordocument import GuiTextDocument from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.wheeleventfilter import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
+1 -1
View File
@@ -46,7 +46,7 @@ from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
from novelwriter.extensions.wheeleventfilter import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
+1 -1
View File
@@ -50,7 +50,7 @@ class GuiTextDocument(QTextDocument):
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiTextDocument") logger.debug("Delete: GuiTextDocument")
return return
+4 -13
View File
@@ -3,8 +3,7 @@ novelWriter GUI Main Menu
=========================== ===========================
File History: File History:
Created: 2019-04-27 [0.0.1] GuiMainMenu Created: 2019-04-27 [0.0.1]
Created: 2023-11-28 [2.2] StatusTipFilter
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen Copyright 20182023, Veronica Berglyd Olsen
@@ -29,14 +28,15 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from PyQt5.QtGui import QDesktopServices, QStatusTipEvent from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QEvent, QObject, QUrl, pyqtSignal, pyqtSlot from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QMenuBar, QAction from PyQt5.QtWidgets import QMenuBar, QAction
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
from novelwriter.common import openExternalPath from novelwriter.common import openExternalPath
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
from novelwriter.extensions.eventfilters import StatusTipFilter
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -971,12 +971,3 @@ class GuiMainMenu(QMenuBar):
return return
# END Class GuiMainMenu # END Class GuiMainMenu
class StatusTipFilter(QObject):
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
"""Filter out status tip events."""
return True if isinstance(event, QStatusTipEvent) else super().eventFilter(obj, event)
# END Class StatusTipFilter
+1 -1
View File
@@ -1650,7 +1650,7 @@ class _TreeContextMenu(QMenu):
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _TreeContextMenu") logger.debug("Delete: _TreeContextMenu")
return return
+4 -2
View File
@@ -27,12 +27,13 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwView from novelwriter.enum import nwView
from novelwriter.extensions.eventfilters import StatusTipFilter
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -54,6 +55,7 @@ class GuiSideBar(QWidget):
iPx = CONFIG.pxInt(24) iPx = CONFIG.pxInt(24)
iconSize = QSize(iPx, iPx) iconSize = QSize(iPx, iPx)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.installEventFilter(StatusTipFilter(mainGui))
# Buttons # Buttons
self.tbProject = QToolButton(self) self.tbProject = QToolButton(self)
@@ -162,7 +164,7 @@ class GuiSideBar(QWidget):
class _PopRightMenu(QMenu): class _PopRightMenu(QMenu):
def event(self, event: QEvent): def event(self, event: QEvent) -> bool:
"""Overload the show event and move the menu popup location.""" """Overload the show event and move the menu popup location."""
if event.type() == QEvent.Show: if event.type() == QEvent.Show:
parent = self.parent() parent = self.parent()
+40 -26
View File
@@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
class SharedData(QObject): class SharedData(QObject):
__slots__ = ( __slots__ = (
"_gui", "_theme", "_project", "_spelling", "_lockedBy", "_alert", "_gui", "_theme", "_project", "_spelling", "_lockedBy", "_lastAlert",
"_idleTime", "_idleRefTime", "_idleTime", "_idleRefTime",
) )
@@ -68,7 +68,7 @@ class SharedData(QObject):
# Settings # Settings
self._lockedBy = None self._lockedBy = None
self._alert = None self._lastAlert = ""
self._idleTime = 0.0 self._idleTime = 0.0
self._idleRefTime = time() self._idleRefTime = time()
@@ -122,9 +122,9 @@ class SharedData(QObject):
return self._idleTime return self._idleTime
@property @property
def alert(self) -> _GuiAlert | None: def lastAlert(self) -> str:
"""Return a pointer to the last alert box.""" """Return the last alert message."""
return self._alert return self._lastAlert
## ##
# Methods # Methods
@@ -238,44 +238,53 @@ class SharedData(QObject):
def info(self, text: str, info: str = "", details: str = "", log: bool = True) -> None: def info(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
"""Open an information alert box.""" """Open an information alert box."""
self._alert = _GuiAlert(self.mainGui, self.theme) alert = _GuiAlert(self.mainGui, self.theme)
self._alert.setMessage(text, info, details) alert.setMessage(text, info, details)
self._alert.setAlertType(_GuiAlert.INFO, False) alert.setAlertType(_GuiAlert.INFO, False)
self._lastAlert = alert.logMessage
if log: if log:
logger.info(self._alert.logMessage, stacklevel=2) logger.info(self._lastAlert, stacklevel=2)
self._alert.exec_() alert.exec_()
alert.deleteLater()
return return
def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None: def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
"""Open a warning alert box.""" """Open a warning alert box."""
self._alert = _GuiAlert(self.mainGui, self.theme) alert = _GuiAlert(self.mainGui, self.theme)
self._alert.setMessage(text, info, details) alert.setMessage(text, info, details)
self._alert.setAlertType(_GuiAlert.WARN, False) alert.setAlertType(_GuiAlert.WARN, False)
self._lastAlert = alert.logMessage
if log: if log:
logger.warning(self._alert.logMessage, stacklevel=2) logger.warning(self._lastAlert, stacklevel=2)
self._alert.exec_() alert.exec_()
alert.deleteLater()
return return
def error(self, text: str, info: str = "", details: str = "", log: bool = True, def error(self, text: str, info: str = "", details: str = "", log: bool = True,
exc: Exception | None = None) -> None: exc: Exception | None = None) -> None:
"""Open an error alert box.""" """Open an error alert box."""
self._alert = _GuiAlert(self.mainGui, self.theme) alert = _GuiAlert(self.mainGui, self.theme)
self._alert.setMessage(text, info, details) alert.setMessage(text, info, details)
self._alert.setAlertType(_GuiAlert.ERROR, False) alert.setAlertType(_GuiAlert.ERROR, False)
if exc: if exc:
self._alert.setException(exc) alert.setException(exc)
self._lastAlert = alert.logMessage
if log: if log:
logger.error(self._alert.logMessage, stacklevel=2) logger.error(self._lastAlert, stacklevel=2)
self._alert.exec_() alert.exec_()
alert.deleteLater()
return return
def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
"""Open a question box.""" """Open a question box."""
self._alert = _GuiAlert(self.mainGui, self.theme) alert = _GuiAlert(self.mainGui, self.theme)
self._alert.setMessage(text, info, details) alert.setMessage(text, info, details)
self._alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
self._alert.exec_() self._lastAlert = alert.logMessage
return self._alert.result() == QMessageBox.Yes alert.exec_()
isYes = alert.result() == QMessageBox.StandardButton.Yes
alert.deleteLater()
return isYes
## ##
# Internal Functions # Internal Functions
@@ -312,6 +321,11 @@ class _GuiAlert(QMessageBox):
super().__init__(parent=parent) super().__init__(parent=parent)
self._theme = theme self._theme = theme
self._message = "" self._message = ""
logger.debug("Ready: _GuiAlert")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _GuiAlert")
return return
@property @property
+17 -23
View File
@@ -66,7 +66,7 @@ class GuiManuscript(QDialog):
D_KEY = Qt.ItemDataRole.UserRole D_KEY = Qt.ItemDataRole.UserRole
def __init__(self, mainGui: GuiMain): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiManuscript") logger.debug("Create: GuiManuscript")
@@ -173,7 +173,7 @@ class GuiManuscript(QDialog):
self.btnBuild.clicked.connect(self._buildManuscript) self.btnBuild.clicked.connect(self._buildManuscript)
self.btnClose = QPushButton(self.tr("Close")) self.btnClose = QPushButton(self.tr("Close"))
self.btnClose.clicked.connect(self._doClose) self.btnClose.clicked.connect(self.close)
self.processBox = QGridLayout() self.processBox = QGridLayout()
self.processBox.addWidget(self.btnPreview, 0, 0) self.processBox.addWidget(self.btnPreview, 0, 0)
@@ -217,11 +217,11 @@ class GuiManuscript(QDialog):
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscript") logger.debug("Delete: GuiManuscript")
return return
def loadContent(self): def loadContent(self) -> None:
"""Load dialog content from project data.""" """Load dialog content from project data."""
if len(self._builds) == 0: if len(self._builds) == 0:
build = BuildSettings() build = BuildSettings()
@@ -255,7 +255,7 @@ class GuiManuscript(QDialog):
# Events # Events
## ##
def closeEvent(self, event: QCloseEvent): def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window so we can save GUI """Capture the user closing the window so we can save GUI
settings. We also check that we don't have a build settings settings. We also check that we don't have a build settings
dialog open. dialog open.
@@ -274,7 +274,7 @@ class GuiManuscript(QDialog):
## ##
@pyqtSlot() @pyqtSlot()
def _createNewBuild(self): def _createNewBuild(self) -> None:
"""Open the build settings dialog for a new build.""" """Open the build settings dialog for a new build."""
build = BuildSettings() build = BuildSettings()
build.setName(self.tr("My Manuscript")) build.setName(self.tr("My Manuscript"))
@@ -282,7 +282,7 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _editSelectedBuild(self): def _editSelectedBuild(self) -> None:
"""Edit the currently selected build settings entry.""" """Edit the currently selected build settings entry."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if build is not None: if build is not None:
@@ -299,7 +299,7 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _deleteSelectedBuild(self): def _deleteSelectedBuild(self) -> None:
"""Delete the currently selected build settings entry.""" """Delete the currently selected build settings entry."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if build is not None: if build is not None:
@@ -309,7 +309,7 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot(BuildSettings) @pyqtSlot(BuildSettings)
def _processNewSettings(self, build: BuildSettings): def _processNewSettings(self, build: BuildSettings) -> None:
"""Process new build settings from the settings dialog.""" """Process new build settings from the settings dialog."""
self._builds.setBuild(build) self._builds.setBuild(build)
self._updateBuildItem(build) self._updateBuildItem(build)
@@ -319,7 +319,7 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _generatePreview(self): def _generatePreview(self) -> None:
"""Run the document builder on the current build settings for """Run the document builder on the current build settings for
the preview widget. the preview widget.
""" """
@@ -359,7 +359,7 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _buildManuscript(self): def _buildManuscript(self) -> None:
"""Open the build dialog and build the manuscript.""" """Open the build dialog and build the manuscript."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if isinstance(build, BuildSettings): if isinstance(build, BuildSettings):
@@ -373,24 +373,18 @@ class GuiManuscript(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _printDocument(self): def _printDocument(self) -> None:
"""Open the print preview dialog.""" """Open the print preview dialog."""
thePreview = QPrintPreviewDialog(self) thePreview = QPrintPreviewDialog(self)
thePreview.paintRequested.connect(self.docPreview.printPreview) thePreview.paintRequested.connect(self.docPreview.printPreview)
thePreview.exec_() thePreview.exec_()
return return
@pyqtSlot()
def _doClose(self):
"""Forward the close button to the default close method."""
self.close()
return
## ##
# Internal Functions # Internal Functions
## ##
def _updatePreview(self, data: dict, build: BuildSettings): def _updatePreview(self, data: dict, build: BuildSettings) -> None:
"""Update the preview widget and set relevant values.""" """Update the preview widget and set relevant values."""
self.docPreview.setContent(data) self.docPreview.setContent(data)
self.docPreview.setBuildName(build.name) self.docPreview.setBuildName(build.name)
@@ -415,7 +409,7 @@ class GuiManuscript(QDialog):
return build return build
return None return None
def _saveSettings(self): def _saveSettings(self) -> None:
"""Save the user GUI settings.""" """Save the user GUI settings."""
buildOrder = [] buildOrder = []
for i in range(self.buildList.count()): for i in range(self.buildList.count()):
@@ -454,7 +448,7 @@ class GuiManuscript(QDialog):
return return
def _openSettingsDialog(self, build: BuildSettings): def _openSettingsDialog(self, build: BuildSettings) -> None:
"""Open the build settings dialog.""" """Open the build settings dialog."""
for obj in self.mainGui.children(): for obj in self.mainGui.children():
# Don't open a second dialog if one exists # Don't open a second dialog if one exists
@@ -475,7 +469,7 @@ class GuiManuscript(QDialog):
return return
def _updateBuildsList(self): def _updateBuildsList(self) -> None:
"""Update the list of available builds.""" """Update the list of available builds."""
self.buildList.clear() self.buildList.clear()
for key, name in self._builds.builds(): for key, name in self._builds.builds():
@@ -487,7 +481,7 @@ class GuiManuscript(QDialog):
self._buildMap[key] = bItem self._buildMap[key] = bItem
return return
def _updateBuildItem(self, build: BuildSettings): def _updateBuildItem(self, build: BuildSettings) -> None:
"""Update the entry of a specific build item.""" """Update the entry of a specific build item."""
bItem = self._buildMap.get(build.buildID, None) bItem = self._buildMap.get(build.buildID, None)
if isinstance(bItem, QListWidgetItem): if isinstance(bItem, QListWidgetItem):
+1 -1
View File
@@ -80,7 +80,7 @@ class GuiProjectWizard(QWizard):
return return
def __del__(self): # pragma: no cover def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectWizard") logger.debug("Delete: GuiProjectWizard")
return return
+7 -16
View File
@@ -21,8 +21,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import time import time
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
import pytest import pytest
from pathlib import Path from pathlib import Path
@@ -31,14 +29,16 @@ from xml.etree import ElementTree as ET
from tools import writeFile from tools import writeFile
from mocked import causeOSError from mocked import causeOSError
from novelwriter.guimain import GuiMain from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QUrl
from novelwriter.common import ( from novelwriter.common import (
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath, checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, formatInt, formatTime, checkString, checkStringNone, checkUuid, formatInt, formatTime,
formatTimeStamp, fuzzyTime, getFileSize, getGuiItem, hexToInt, isHandle, formatTimeStamp, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode, isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
makeFileNameSafe, minmax, numberToRoman, NWConfigParser, openExternalPath, numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified,
readTextFile, simplified, transferCase, xmlIndent, yesNo transferCase, xmlIndent, yesNo
) )
@@ -655,15 +655,6 @@ def testBaseCommon_openExternalPath(monkeypatch, tstPaths):
# END Test testBaseCommon_openExternalPath # END Test testBaseCommon_openExternalPath
@pytest.mark.base
def testBaseCommon_getGuiItem(nwGUI):
"""Check the GUI item function."""
assert getGuiItem("gibberish") is None
assert isinstance(getGuiItem("GuiMain"), GuiMain)
# END Test testBaseCommon_getGuiItem
@pytest.mark.base @pytest.mark.base
def testBaseCommon_NWConfigParser(fncPath): def testBaseCommon_NWConfigParser(fncPath):
"""Test the NWConfigParser subclass.""" """Test the NWConfigParser subclass."""
+9 -29
View File
@@ -22,13 +22,13 @@ from __future__ import annotations
import pytest import pytest
from tools import buildTestProject
from mocked import MockGuiMain, MockTheme from mocked import MockGuiMain, MockTheme
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from novelwriter.shared import SharedData
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.shared import SharedData, _GuiAlert
from tests.tools import buildTestProject
@pytest.mark.base @pytest.mark.base
@@ -62,8 +62,6 @@ def testBaseSharedData_Init():
assert shared.projectIdleTime == 0.0 assert shared.projectIdleTime == 0.0
assert shared.projectLock is None assert shared.projectLock is None
assert shared.alert is None
# END Test testBaseSharedData_Init # END Test testBaseSharedData_Init
@@ -124,7 +122,7 @@ def testBaseSharedData_Projects(fncPath, caplog):
@pytest.mark.base @pytest.mark.base
def testBaseSharedData_Alerts(monkeypatch, caplog): def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog):
"""Test SharedData class alert helper functions.""" """Test SharedData class alert helper functions."""
monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
@@ -135,56 +133,38 @@ def testBaseSharedData_Alerts(monkeypatch, caplog):
mockTheme = MockTheme() mockTheme = MockTheme()
shared.initSharedData(mockGui, mockTheme) # type: ignore shared.initSharedData(mockGui, mockTheme) # type: ignore
assert shared.alert is None assert shared.lastAlert == ""
# Info box # Info box
caplog.clear() caplog.clear()
shared.info("Hello World", info="foo", details="bar") shared.info("Hello World", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert) assert shared.lastAlert == "Hello World foo bar"
assert shared.alert.text() == "Hello World"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("INFO") assert caplog.text.strip().startswith("INFO")
assert caplog.text.strip().endswith("Hello World foo bar") assert caplog.text.strip().endswith("Hello World foo bar")
shared._alert = None
# Warning box # Warning box
caplog.clear() caplog.clear()
shared.warn("Oops!", info="foo", details="bar") shared.warn("Oops!", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert) assert shared.lastAlert == "Oops! foo bar"
assert shared.alert.text() == "Oops!"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("WARNING") assert caplog.text.strip().startswith("WARNING")
assert caplog.text.strip().endswith("Oops! foo bar") assert caplog.text.strip().endswith("Oops! foo bar")
shared._alert = None
# Error box # Error box
caplog.clear() caplog.clear()
shared.error("Oh noes!", info="foo", details="bar") shared.error("Oh noes!", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert) assert shared.lastAlert == "Oh noes! foo bar"
assert shared.alert.text() == "Oh noes!"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("ERROR") assert caplog.text.strip().startswith("ERROR")
assert caplog.text.strip().endswith("Oh noes! foo bar") assert caplog.text.strip().endswith("Oh noes! foo bar")
shared._alert = None
# Error box with exception # Error box with exception
caplog.clear() caplog.clear()
shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!")) shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!"))
assert isinstance(shared.alert, _GuiAlert) assert shared.lastAlert == "Oh noes! foo bar"
assert shared.alert.text() == "Oh noes!"
assert shared.alert.informativeText() == "foo<br><b>Exception</b>: Boom!"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("ERROR") assert caplog.text.strip().startswith("ERROR")
assert caplog.text.strip().endswith("Oh noes! foo bar") assert caplog.text.strip().endswith("Oh noes! foo bar")
shared._alert = None
# Question box # Question box
assert shared.question("Why?") is True assert shared.question("Why?") is True
assert isinstance(shared.alert, _GuiAlert) assert shared.lastAlert == "Why?"
assert shared.alert.text() == "Why?"
shared._alert = None
# END Test testBaseSharedData_Alerts # END Test testBaseSharedData_Alerts
+6 -12
View File
@@ -206,40 +206,35 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE))
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "Project file does not appear" in SHARED.lastAlert
assert "Project file does not appear" in lastMsg
# Unknown project file version # Unknown project file version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION))
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "Unknown or unsupported novelWriter project file" in SHARED.lastAlert
assert "Unknown or unsupported novelWriter project file" in lastMsg
# Other parse error # Other parse error
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE))
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "Failed to parse project xml" in SHARED.lastAlert
assert "Failed to parse project xml" in lastMsg
# Won't convert legacy file # Won't convert legacy file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "The file format of your project is about to be" in SHARED.lastAlert
assert "The file format of your project is about to be" in lastMsg
# Won't open project from newer version # Won't open project from newer version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "This project was saved by a newer version" in SHARED.lastAlert
assert "This project was saved by a newer version" in lastMsg
# Fail checking items should still pass # Fail checking items should still pass
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -254,8 +249,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True) mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True)
theProject.index._indexBroken = True theProject.index._indexBroken = True
assert theProject.openProject(fncPath) is True assert theProject.openProject(fncPath) is True
lastMsg = SHARED.alert.logMessage if SHARED.alert else "" assert "The file format of your project is about to be" in SHARED.lastAlert
assert "The file format of your project is about to be" in lastMsg
assert theProject.index._indexBroken is False assert theProject.index._indexBroken is False
theProject.closeProject() theProject.closeProject()
@@ -26,7 +26,7 @@ from PyQt5.QtGui import QKeyEvent, QWheelEvent
from PyQt5.QtCore import QEvent, QObject, QPoint, Qt from PyQt5.QtCore import QEvent, QObject, QPoint, Qt
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from novelwriter.extensions.wheeleventfilter import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
class MockWidget(QWidget): class MockWidget(QWidget):
@@ -42,7 +42,7 @@ class MockWidget(QWidget):
@pytest.mark.gui @pytest.mark.gui
def testExtWheelEventFilter_Main(): def testExtEventFilters_WheelEventFilter():
"""Test the WheelEventFilter class.""" """Test the WheelEventFilter class."""
obj = QObject() obj = QObject()
widget = MockWidget() widget = MockWidget()
@@ -65,4 +65,4 @@ def testExtWheelEventFilter_Main():
eFilter.eventFilter(obj, event) eFilter.eventFilter(obj, event)
assert widget.count == 1 assert widget.count == 1
# END Test testExtWheelEventFilter_Main # END Test testExtEventFilters_WheelEventFilter
+1 -2
View File
@@ -656,8 +656,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger)
path = str(projPath / "content" / "000000000000f.nwd") path = str(projPath / "content" / "000000000000f.nwd")
logMsg = SHARED.alert.logMessage if SHARED.alert else "" assert SHARED.lastAlert.endswith(f"File Location: {path}")
assert logMsg.endswith(f"File Location: {path}")
# qtbot.stop() # qtbot.stop()
+4 -6
View File
@@ -44,8 +44,7 @@ def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath):
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(enchant, "get_user_config_dir", lambda *a: causeException) mp.setattr(enchant, "get_user_config_dir", lambda *a: causeException)
nwGUI.showDictionariesDialog() nwGUI.showDictionariesDialog()
assert SHARED.alert is not None assert SHARED.lastAlert == "Could not initialise the dialog."
assert SHARED.alert.logMessage == "Could not initialise the dialog."
# Open the tool # Open the tool
nwGUI.showDictionariesDialog() nwGUI.showDictionariesDialog()
@@ -57,17 +56,16 @@ def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath):
assert nwDicts.inPath.text() == str(fncPath) assert nwDicts.inPath.text() == str(fncPath)
# Allow Open Dir # Allow Open Dir
SHARED._alert = None SHARED._lastAlert = ""
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QDesktopServices, "openUrl", lambda *a: None) mp.setattr(QDesktopServices, "openUrl", lambda *a: None)
nwDicts._doOpenInstallLocation() nwDicts._doOpenInstallLocation()
assert SHARED.alert is None assert SHARED.lastAlert == ""
# Fail Open Dir # Fail Open Dir
nwDicts.inPath.setText("/foo/bar") nwDicts.inPath.setText("/foo/bar")
nwDicts._doOpenInstallLocation() nwDicts._doOpenInstallLocation()
assert SHARED.alert is not None assert SHARED.lastAlert == "Path not found."
assert SHARED.alert.logMessage == "Path not found."
nwDicts.inPath.setText(str(fncPath)) nwDicts.inPath.setText(str(fncPath))
# Create Mock Dicts # Create Mock Dicts