From fa0bef55000c95445279a121f8d93d5273d7e08e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:12:09 +0100 Subject: [PATCH 1/5] Extend and refactor current open document highlighting --- novelwriter/gui/doceditor.py | 28 +++++++++++++--------- novelwriter/gui/noveltree.py | 45 +++++++++++++++++++++--------------- novelwriter/gui/projtree.py | 20 ++++++++++++++++ novelwriter/guimain.py | 9 ++++---- 4 files changed, 68 insertions(+), 34 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 04e9cf94..9f9beeea 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -90,20 +90,21 @@ class GuiDocEditor(QPlainTextEdit): ) # Custom Signals - statusMessage = pyqtSignal(str) + closeEditorRequest = pyqtSignal() docCountsChanged = pyqtSignal(str, int, int, int) docTextChanged = pyqtSignal(str, float) editedStatusChanged = pyqtSignal(bool) + itemHandleChanged = pyqtSignal(str) loadDocumentTagRequest = pyqtSignal(str, Enum) - novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) - spellCheckStateChanged = pyqtSignal(bool) - closeDocumentRequest = pyqtSignal() - toggleFocusModeRequest = pyqtSignal() - requestProjectItemSelected = pyqtSignal(str, bool) - requestProjectItemRenamed = pyqtSignal(str, str) + novelStructureChanged = pyqtSignal() requestNewNoteCreation = pyqtSignal(str, nwItemClass) requestNextDocument = pyqtSignal(str, bool) + requestProjectItemRenamed = pyqtSignal(str, str) + requestProjectItemSelected = pyqtSignal(str, bool) + spellCheckStateChanged = pyqtSignal(bool) + toggleFocusModeRequest = pyqtSignal() + updateStatusMessage = pyqtSignal(str) def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -271,6 +272,8 @@ class GuiDocEditor(QPlainTextEdit): self.docFooter.setHandle(self._docHandle) self.docToolBar.setVisible(False) + self.itemHandleChanged.emit("") + return def updateTheme(self) -> None: @@ -430,12 +433,15 @@ class GuiDocEditor(QPlainTextEdit): self.setDocumentChanged(False) self._qDocument.clearUndoRedoStacks() self.docToolBar.setVisible(CONFIG.showEditToolBar) + self.itemHandleChanged.emit(tHandle) QApplication.restoreOverrideCursor() # Update the status bar if self._nwItem is not None: - self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName)) + self.updateStatusMessage.emit( + self.tr("Opened Document: {0}").format(self._nwItem.itemName) + ) return True @@ -506,7 +512,7 @@ class GuiDocEditor(QPlainTextEdit): self.docFooter.updateInfo() # Update the status bar - self.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName)) + self.updateStatusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName)) return True @@ -701,7 +707,7 @@ class GuiDocEditor(QPlainTextEdit): self._qDocument.syntaxHighlighter.rehighlight() QApplication.restoreOverrideCursor() logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) - self.statusMessage.emit(self.tr("Spell check complete")) + self.updateStatusMessage.emit(self.tr("Spell check complete")) return ## @@ -1274,7 +1280,7 @@ class GuiDocEditor(QPlainTextEdit): @pyqtSlot() def _closeCurrentDocument(self) -> None: """Close the document. Forwarded to the main Gui.""" - self.closeDocumentRequest.emit() + self.closeEditorRequest.emit() self.docToolBar.setVisible(False) return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 5b33a5fb..c2a28c2b 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -87,7 +87,6 @@ class GuiNovelView(QWidget): # Function Mappings self.getSelectedHandle = self.novelTree.getSelectedHandle - self.setActiveHandle = self.novelTree.setActiveHandle return @@ -163,6 +162,12 @@ class GuiNovelView(QWidget): # Public Slots ## + @pyqtSlot(str) + def setActiveHandle(self, tHandle: str) -> None: + """Highlight the rows associated with a given handle.""" + self.novelTree.setActiveHandle(tHandle) + return + @pyqtSlot() def refreshTree(self) -> None: """Refresh the current tree.""" @@ -367,11 +372,11 @@ class GuiNovelTree(QTreeWidget): self.novelView = novelView # Internal Variables - self._treeMap = {} self._lastBuild = 0 self._lastCol = NovelTreeColumn.POV self._lastColSize = 0.25 self._actHandle = None + self._treeMap: dict[str, QTreeWidgetItem] = {} # Cached Strings self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) @@ -540,25 +545,29 @@ class GuiNovelTree(QTreeWidget): self._lastColSize = minmax(colSize, 15, 75)/100.0 return - def setActiveHandle(self, tHandle: str | None, doScroll: bool = False) -> None: + def setActiveHandle(self, tHandle: str | None) -> None: """Highlight the rows associated with a given handle.""" didScroll = False - self._actHandle = tHandle - for i in range(self.topLevelItemCount()): - if tItem := self.topLevelItem(i): - if tItem.data(self.C_DATA, self.D_HANDLE) == tHandle: - tItem.setBackground(self.C_TITLE, self.palette().alternateBase()) - tItem.setBackground(self.C_WORDS, self.palette().alternateBase()) - tItem.setBackground(self.C_EXTRA, self.palette().alternateBase()) - tItem.setBackground(self.C_MORE, self.palette().alternateBase()) - if doScroll and not didScroll: - self.scrollToItem(tItem, QAbstractItemView.ScrollHint.PositionAtCenter) + brushOn = self.palette().alternateBase() + brushOff = self.palette().base() + if pHandle := self._actHandle: + for key, item in self._treeMap.items(): + if key.startswith(pHandle): + item.setBackground(self.C_TITLE, brushOff) + item.setBackground(self.C_WORDS, brushOff) + item.setBackground(self.C_EXTRA, brushOff) + item.setBackground(self.C_MORE, brushOff) + if tHandle: + for key, item in self._treeMap.items(): + if key.startswith(tHandle): + item.setBackground(self.C_TITLE, brushOn) + item.setBackground(self.C_WORDS, brushOn) + item.setBackground(self.C_EXTRA, brushOn) + item.setBackground(self.C_MORE, brushOn) + if not didScroll: + self.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtCenter) didScroll = True - else: - tItem.setBackground(self.C_TITLE, self.palette().base()) - tItem.setBackground(self.C_WORDS, self.palette().base()) - tItem.setBackground(self.C_EXTRA, self.palette().base()) - tItem.setBackground(self.C_MORE, self.palette().base()) + self._actHandle = tHandle or None return ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9863fb83..6bcae09d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -211,6 +211,12 @@ class GuiProjectView(QWidget): self.projTree.setSelectedHandle(tHandle, doScroll=doScroll) return + @pyqtSlot(str) + def setActiveHandle(self, tHandle: str | None) -> None: + """Highlight the active handle.""" + self.projTree.setActiveHandle(tHandle) + return + @pyqtSlot(str) def updateItemValues(self, tHandle: str) -> None: """Update tree item.""" @@ -500,6 +506,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap: dict[str, QTreeWidgetItem] = {} self._timeChanged = 0.0 self._popAlert = None + self._actHandle = None # Cached Translations self.trActive = self.tr("Active") @@ -1144,6 +1151,19 @@ class GuiProjectTree(QTreeWidget): return True + def setActiveHandle(self, tHandle: str | None) -> None: + """Highlight the rows associated with a given handle.""" + brushOn = self.palette().alternateBase() + brushOff = self.palette().base() + if (pHandle := self._actHandle) and (item := self._treeMap.get(pHandle)): + for i in range(self.columnCount()): + item.setBackground(i, brushOff) + if tHandle and (item := self._treeMap.get(tHandle)): + for i in range(self.columnCount()): + item.setBackground(i, brushOn) + self._actHandle = tHandle or None + return + def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None: """Iterate through items below tHandle and change expanded status for all child items. If tHandle is None, it affects the diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e29d491d..dbdc2b26 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -249,11 +249,13 @@ class GuiMain(QMainWindow): self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection) self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox) - self.docEditor.closeDocumentRequest.connect(self.closeDocEditor) + self.docEditor.closeEditorRequest.connect(self.closeDocEditor) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.docTextChanged.connect(self.projSearch.textChanged) self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus) + self.docEditor.itemHandleChanged.connect(self.novelView.setActiveHandle) + self.docEditor.itemHandleChanged.connect(self.projView.setActiveHandle) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) @@ -262,8 +264,8 @@ class GuiMain(QMainWindow): self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem) self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle) self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState) - self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage) self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode) + self.docEditor.updateStatusMessage.connect(self.mainStatus.setStatusMessage) self.docViewer.closeDocumentRequest.connect(self.closeDocViewer) self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle) @@ -518,8 +520,6 @@ class GuiMain(QMainWindow): SHARED.setFocusMode(False) self.saveDocument() self.docEditor.clearEditor() - if not beforeOpen: - self.novelView.setActiveHandle(None) return def openDocument( @@ -554,7 +554,6 @@ class GuiMain(QMainWindow): if self.docEditor.loadText(tHandle, tLine): SHARED.project.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) - self.novelView.setActiveHandle(tHandle, doScroll=doScroll) if changeFocus: self.docEditor.setFocus() else: From a2b09fdfecca116aeb8df31ee3ab46a9b9a6cdf9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:13:13 +0100 Subject: [PATCH 2/5] Update various features in the doc editor using new helper code --- novelwriter/gui/doceditor.py | 116 ++++++++++++++-------------- novelwriter/gui/mainmenu.py | 17 ++-- novelwriter/guimain.py | 34 ++++---- novelwriter/shared.py | 14 +++- tests/test_base/test_base_shared.py | 18 +++++ tests/test_gui/test_gui_mainmenu.py | 9 --- 6 files changed, 104 insertions(+), 104 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 9f9beeea..a4f76529 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -38,13 +38,12 @@ from enum import Enum from time import time from PyQt5.QtCore import ( - QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, QUrl, - pyqtSignal, pyqtSlot + QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, + pyqtSlot ) from PyQt5.QtGui import ( - QColor, QCursor, QDesktopServices, QKeyEvent, QKeySequence, QMouseEvent, - QPalette, QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument, - QTextOption + QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, + QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption ) from PyQt5.QtWidgets import ( QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, @@ -52,10 +51,13 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import minmax, transferCase +from novelwriter.common import minmax, qtLambda, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode from novelwriter.core.document import NWDocument -from novelwriter.enum import nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary +from novelwriter.enum import ( + nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwItemType, + nwTrinary +) from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton @@ -392,9 +394,12 @@ class GuiDocEditor(QPlainTextEdit): """ self._nwDocument = SHARED.project.storage.getDocument(tHandle) self._nwItem = self._nwDocument.nwItem + if not ((nwItem := self._nwItem) and nwItem.itemType == nwItemType.FILE): + logger.debug("Requested item '%s' is not a document", tHandle) + self.clearEditor() + return False - docText = self._nwDocument.readDocument() - if docText is None: + if (docText := self._nwDocument.readDocument()) is None: # There was an I/O error self.clearEditor() return False @@ -415,10 +420,10 @@ class GuiDocEditor(QPlainTextEdit): self.setReadOnly(False) self.updateDocMargins() - if tLine is None and self._nwItem is not None: - self.setCursorPosition(self._nwItem.cursorPos) - elif isinstance(tLine, int): + if isinstance(tLine, int): self.setCursorLine(tLine) + else: + self.setCursorPosition(nwItem.cursorPos) self.docHeader.setHandle(tHandle) self.docFooter.setHandle(tHandle) @@ -433,15 +438,16 @@ class GuiDocEditor(QPlainTextEdit): self.setDocumentChanged(False) self._qDocument.clearUndoRedoStacks() self.docToolBar.setVisible(CONFIG.showEditToolBar) + + # Process State Changes + SHARED.project.data.setLastHandle(tHandle, "editor") self.itemHandleChanged.emit(tHandle) + # Finalise QApplication.restoreOverrideCursor() - - # Update the status bar - if self._nwItem is not None: - self.updateStatusMessage.emit( - self.tr("Opened Document: {0}").format(self._nwItem.itemName) - ) + self.updateStatusMessage.emit( + self.tr("Opened Document: {0}").format(nwItem.itemName) + ) return True @@ -995,7 +1001,7 @@ class GuiDocEditor(QPlainTextEdit): cursor = self.cursorForPosition(event.pos()) mData, mType = self._qDocument.metaDataAtPos(cursor.position()) if mData and mType == "url": - self._openWebsite(mData) + SHARED.openWebsite(mData) else: self._processTag(cursor) super().mouseReleaseEvent(event) @@ -1126,48 +1132,48 @@ class GuiDocEditor(QPlainTextEdit): ctxMenu.setObjectName("ContextMenu") if pBlock.userState() == BLOCK_TITLE: action = ctxMenu.addAction(self.tr("Set as Document Name")) - action.triggered.connect(lambda: self._emitRenameItem(pBlock)) + action.triggered.connect(qtLambda(self._emitRenameItem, pBlock)) # URL (mData, mType) = self._qDocument.metaDataAtPos(pCursor.position()) if mData and mType == "url": action = ctxMenu.addAction(self.tr("Open URL")) - action.triggered.connect(lambda: self._openWebsite(mData)) + action.triggered.connect(qtLambda(SHARED.openWebsite, mData)) ctxMenu.addSeparator() # Follow status = self._processTag(cursor=pCursor, follow=False) if status == nwTrinary.POSITIVE: action = ctxMenu.addAction(self.tr("Follow Tag")) - action.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True)) + action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True)) ctxMenu.addSeparator() elif status == nwTrinary.NEGATIVE: action = ctxMenu.addAction(self.tr("Create Note for Tag")) - action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True)) + action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True)) ctxMenu.addSeparator() # Cut, Copy and Paste if uCursor.hasSelection(): action = ctxMenu.addAction(self.tr("Cut")) - action.triggered.connect(lambda: self.docAction(nwDocAction.CUT)) + action.triggered.connect(qtLambda(self.docAction, nwDocAction.CUT)) action = ctxMenu.addAction(self.tr("Copy")) - action.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) + action.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY)) action = ctxMenu.addAction(self.tr("Paste")) - action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE)) + action.triggered.connect(qtLambda(self.docAction, nwDocAction.PASTE)) ctxMenu.addSeparator() # Selections action = ctxMenu.addAction(self.tr("Select All")) - action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) + action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL)) action = ctxMenu.addAction(self.tr("Select Word")) - action.triggered.connect( - lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos) - ) + action.triggered.connect(qtLambda( + self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, pos, + )) action = ctxMenu.addAction(self.tr("Select Paragraph")) - action.triggered.connect(lambda: self._makePosSelection( - QTextCursor.SelectionType.BlockUnderCursor, pos) - ) + action.triggered.connect(qtLambda( + self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos + )) # Spell Checking if SHARED.project.data.spellCheck: @@ -1183,18 +1189,16 @@ class GuiDocEditor(QPlainTextEdit): ctxMenu.addAction(self.tr("Spelling Suggestion(s)")) for option in suggest[:15]: action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}") - action.triggered.connect( - lambda _, option=option: self._correctWord(sCursor, option) - ) + action.triggered.connect(qtLambda(self._correctWord, sCursor, option)) else: trNone = self.tr("No Suggestions") ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}") ctxMenu.addSeparator() action = ctxMenu.addAction(self.tr("Ignore Word")) - action.triggered.connect(lambda: self._addWord(word, block, False)) + action.triggered.connect(qtLambda(self._addWord, word, block, False)) action = ctxMenu.addAction(self.tr("Add Word to Dictionary")) - action.triggered.connect(lambda: self._addWord(word, block, True)) + action.triggered.connect(qtLambda(self._addWord, word, block, True)) # Execute the context menu ctxMenu.exec(self.viewport().mapToGlobal(pos)) @@ -1202,12 +1206,6 @@ class GuiDocEditor(QPlainTextEdit): return - @pyqtSlot(str) - def _openWebsite(self, url: str) -> None: - """Open a URL in the system's default browser.""" - QDesktopServices.openUrl(QUrl(url)) - return - @pyqtSlot() def _runDocumentTasks(self) -> None: """Run timer document tasks.""" @@ -2207,7 +2205,7 @@ class MetaCompleter(QMenu): for value in sorted(options): rep = value + suffix action = self.addAction(value) - action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r)) + action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep)) return True @@ -2307,61 +2305,61 @@ class GuiDocToolBar(QWidget): self.tbBoldMD = NIconToolButton(self, iSz) self.tbBoldMD.setToolTip(self.tr("Markdown Bold")) self.tbBoldMD.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD) + qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD) ) self.tbItalicMD = NIconToolButton(self, iSz) self.tbItalicMD.setToolTip(self.tr("Markdown Italic")) self.tbItalicMD.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC) + qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC) ) self.tbStrikeMD = NIconToolButton(self, iSz) self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough")) self.tbStrikeMD.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE) + qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE) ) self.tbBold = NIconToolButton(self, iSz) self.tbBold.setToolTip(self.tr("Shortcode Bold")) self.tbBold.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD) ) self.tbItalic = NIconToolButton(self, iSz) self.tbItalic.setToolTip(self.tr("Shortcode Italic")) self.tbItalic.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC) ) self.tbStrike = NIconToolButton(self, iSz) self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough")) self.tbStrike.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE) ) self.tbUnderline = NIconToolButton(self, iSz) self.tbUnderline.setToolTip(self.tr("Shortcode Underline")) self.tbUnderline.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE) ) self.tbMark = NIconToolButton(self, iSz) self.tbMark.setToolTip(self.tr("Shortcode Highlight")) self.tbMark.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_MARK) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK) ) self.tbSuperscript = NIconToolButton(self, iSz) self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript")) self.tbSuperscript.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_SUP) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP) ) self.tbSubscript = NIconToolButton(self, iSz) self.tbSubscript.setToolTip(self.tr("Shortcode Subscript")) self.tbSubscript.clicked.connect( - lambda: self.requestDocAction.emit(nwDocAction.SC_SUB) + qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB) ) # Assemble @@ -2825,7 +2823,7 @@ class GuiDocEditHeader(QWidget): self.tbButton = NIconToolButton(self, iSz) self.tbButton.setVisible(False) self.tbButton.setToolTip(self.tr("Toggle Tool Bar")) - self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit()) + self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit)) self.outlineButton = NIconToolButton(self, iSz) self.outlineButton.setVisible(False) @@ -2840,7 +2838,7 @@ class GuiDocEditHeader(QWidget): self.minmaxButton = NIconToolButton(self, iSz) self.minmaxButton.setVisible(False) self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode")) - self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit()) + self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit)) self.closeButton = NIconToolButton(self, iSz) self.closeButton.setVisible(False) @@ -2903,9 +2901,7 @@ class GuiDocEditHeader(QWidget): self.outlineMenu.clear() for number, text in data.items(): action = self.outlineMenu.addAction(text) - action.triggered.connect( - lambda _, number=number: self._gotoBlock(number) - ) + action.triggered.connect(qtLambda(self._gotoBlock, number)) self._docOutline = data logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart)) return diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 4714547d..afecb30a 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -28,8 +28,7 @@ import logging from pathlib import Path from typing import TYPE_CHECKING -from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QDesktopServices +from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtWidgets import QAction, QMenuBar from novelwriter import CONFIG, SHARED @@ -107,12 +106,6 @@ class GuiMainMenu(QMenuBar): self.mainGui.docEditor.toggleSpellCheck(None) return - @pyqtSlot(str) - def _openWebsite(self, url: str) -> None: - """Open a URL in the system's default browser.""" - QDesktopServices.openUrl(QUrl(url)) - return - @pyqtSlot() def _openUserManualFile(self) -> None: """Open the documentation in PDF format.""" @@ -1033,7 +1026,7 @@ class GuiMainMenu(QMenuBar): # Help > User Manual (Online) self.aHelpDocs = self.helpMenu.addAction(self.tr("User Manual (Online)")) self.aHelpDocs.setShortcut("F1") - self.aHelpDocs.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_DOCS)) + self.aHelpDocs.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_DOCS)) self.mainGui.addAction(self.aHelpDocs) # Help > User Manual (PDF) @@ -1048,14 +1041,14 @@ class GuiMainMenu(QMenuBar): # Document > Report an Issue self.aIssue = self.helpMenu.addAction(self.tr("Report an Issue (GitHub)")) - self.aIssue.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_REPORT)) + self.aIssue.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_REPORT)) # Document > Ask a Question self.aQuestion = self.helpMenu.addAction(self.tr("Ask a Question (GitHub)")) - self.aQuestion.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_HELP)) + self.aQuestion.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_HELP)) # Document > Main Website self.aWebsite = self.helpMenu.addAction(self.tr("The novelWriter Website")) - self.aWebsite.triggered.connect(qtLambda(self._openWebsite, nwConst.URL_WEB)) + self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB)) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index dbdc2b26..ac13b356 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.dialogs.wordlist import GuiWordList -from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView +from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwView from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer from novelwriter.gui.docviewerpanel import GuiDocViewerPanel @@ -481,8 +481,7 @@ class GuiMain(QMainWindow): QApplication.processEvents() self.openDocument(lastEdited, doScroll=True) - lastViewed = SHARED.project.data.getLastHandle("viewer") - if lastViewed is not None: + if lastViewed := SHARED.project.data.getLastHandle("viewer"): QApplication.processEvents() self.viewDocument(lastViewed) @@ -512,7 +511,7 @@ class GuiMain(QMainWindow): # Document Actions ## - def closeDocument(self, beforeOpen: bool = False) -> None: + def closeDocument(self) -> None: """Close the document and clear the editor and title field.""" if SHARED.hasProject: # Disable focus mode if it is active @@ -531,12 +530,8 @@ class GuiMain(QMainWindow): doScroll: bool = False ) -> bool: """Open a specific document, optionally at a given line.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - if not tHandle or not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): - logger.debug("Requested item '%s' is not a document", tHandle) + if not (SHARED.hasProject and tHandle): + logger.error("Nothing to open open") return False if sTitle and tLine is None: @@ -546,18 +541,15 @@ class GuiMain(QMainWindow): self._changeView(nwView.EDITOR) if tHandle == self.docEditor.docHandle: self.docEditor.setCursorLine(tLine) - if changeFocus: - self.docEditor.setFocus() - return True - - self.closeDocument(beforeOpen=True) - if self.docEditor.loadText(tHandle, tLine): - SHARED.project.data.setLastHandle(tHandle, "editor") - self.projView.setSelectedHandle(tHandle, doScroll=doScroll) - if changeFocus: - self.docEditor.setFocus() else: - return False + self.closeDocument() + if self.docEditor.loadText(tHandle, tLine): + self.projView.setSelectedHandle(tHandle, doScroll=doScroll) + else: + return False + + if changeFocus: + self.docEditor.setFocus() return True diff --git a/novelwriter/shared.py b/novelwriter/shared.py index e2e2d00a..a42c0ffa 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -30,8 +30,8 @@ from pathlib import Path from time import time from typing import TYPE_CHECKING, TypeVar -from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal -from PyQt5.QtGui import QFont +from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QDesktopServices, QFont from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter @@ -292,6 +292,16 @@ class SharedData(QObject): return widget return None + ## + # Public Slots + ## + + @pyqtSlot(str) + def openWebsite(self, url: str) -> None: + """Open a URL in the system's default browser.""" + QDesktopServices.openUrl(QUrl(url)) + return + ## # Signal Proxy ## diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index bcacba67..cb256fc8 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -20,8 +20,12 @@ along with this program. If not, see . """ from __future__ import annotations +from unittest.mock import MagicMock + import pytest +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget from novelwriter.core.project import NWProject @@ -63,6 +67,20 @@ def testBaseSharedData_Init(): assert shared.projectLock is None +@pytest.mark.base +def testBaseSharedData_Functions(monkeypatch): + """Test SharedData class functions.""" + shared = SharedData() + + # Open URL + with monkeypatch.context() as mp: + openUrl = MagicMock() + mp.setattr(QDesktopServices, "openUrl", openUrl) + shared.openWebsite("http://www.example.com") + assert openUrl.called is True + assert openUrl.call_args[0][0] == QUrl("http://www.example.com") + + @pytest.mark.base def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): """Test SharedData handling of projects.""" diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 34d6901b..fced796e 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -24,7 +24,6 @@ from unittest.mock import MagicMock import pytest -from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices, QTextBlock, QTextCursor from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox @@ -42,14 +41,6 @@ def testGuiMainMenu_Slots(qtbot, monkeypatch, nwGUI, projPath): """Test the main menu slots.""" buildTestProject(nwGUI, projPath) - # Open URL - with monkeypatch.context() as mp: - openUrl = MagicMock() - mp.setattr(QDesktopServices, "openUrl", openUrl) - nwGUI.mainMenu._openWebsite("http://www.example.com") - assert openUrl.called is True - assert openUrl.call_args[0][0] == QUrl("http://www.example.com") - # Open Manual with monkeypatch.context() as mp: openUrl = MagicMock() From 0ba0674b9e063d55dea1b80d5c55a9921eef5571 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:07:48 +0100 Subject: [PATCH 3/5] Some code flow improvements and main GUI test coverage --- novelwriter/gui/noveltree.py | 12 ++-- novelwriter/guimain.py | 72 ++++++++++------------ tests/conftest.py | 4 ++ tests/test_gui/test_gui_guimain.py | 99 ++++++++++++++++++++++++++++-- 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index c2a28c2b..8db8a1e0 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -553,17 +553,13 @@ class GuiNovelTree(QTreeWidget): if pHandle := self._actHandle: for key, item in self._treeMap.items(): if key.startswith(pHandle): - item.setBackground(self.C_TITLE, brushOff) - item.setBackground(self.C_WORDS, brushOff) - item.setBackground(self.C_EXTRA, brushOff) - item.setBackground(self.C_MORE, brushOff) + for i in range(self.columnCount()): + item.setBackground(i, brushOff) if tHandle: for key, item in self._treeMap.items(): if key.startswith(tHandle): - item.setBackground(self.C_TITLE, brushOn) - item.setBackground(self.C_WORDS, brushOn) - item.setBackground(self.C_EXTRA, brushOn) - item.setBackground(self.C_MORE, brushOn) + for i in range(self.columnCount()): + item.setBackground(i, brushOn) if not didScroll: self.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtCenter) didScroll = True diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ac13b356..11dabf78 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -844,35 +844,33 @@ class GuiMain(QMainWindow): def closeMain(self) -> bool: """Save everything, and close novelWriter.""" - if SHARED.hasProject: - msgYes = SHARED.question("%s
%s" % ( - self.tr("Do you want to exit novelWriter?"), - self.tr("Changes are saved automatically.") - )) - if not msgYes: - return False + if SHARED.hasProject and SHARED.question("%s
%s" % ( + self.tr("Do you want to exit novelWriter?"), + self.tr("Changes are saved automatically.") + )): + logger.info("Exiting novelWriter") - logger.info("Exiting novelWriter") + if not SHARED.focusMode: + CONFIG.setMainPanePos(self.splitMain.sizes()) + CONFIG.setOutlinePanePos(self.outlineView.splitSizes()) + if self.docViewerPanel.isVisible(): + CONFIG.setViewPanePos(self.splitView.sizes()) - if not SHARED.focusMode: - CONFIG.setMainPanePos(self.splitMain.sizes()) - CONFIG.setOutlinePanePos(self.outlineView.splitSizes()) - if self.docViewerPanel.isVisible(): - CONFIG.setViewPanePos(self.splitView.sizes()) + CONFIG.showViewerPanel = self.docViewerPanel.isVisible() + wFull = Qt.WindowState.WindowFullScreen + if self.windowState() & wFull != wFull: + # Ignore window size if in full screen mode + CONFIG.setMainWinSize(self.width(), self.height()) - CONFIG.showViewerPanel = self.docViewerPanel.isVisible() - wFull = Qt.WindowState.WindowFullScreen - if self.windowState() & wFull != wFull: - # Ignore window size if in full screen mode - CONFIG.setMainWinSize(self.width(), self.height()) + if SHARED.hasProject: + self.closeProject(True) + CONFIG.saveConfig() - if SHARED.hasProject: - self.closeProject(True) - CONFIG.saveConfig() + QApplication.quit() - QApplication.quit() + return True - return True + return False def closeViewerPanel(self, byUser: bool = True) -> bool: """Close the document view panel.""" @@ -1101,8 +1099,16 @@ class GuiMain(QMainWindow): @pyqtSlot(str, nwDocMode) def _followTag(self, tag: str, mode: nwDocMode) -> None: """Follow a tag after user interaction with a link.""" - tHandle, sTitle = self._getTagSource(tag) - if tHandle is not None: + tHandle, sTitle = SHARED.project.index.getTagSource(tag) + if tHandle is None: + SHARED.error(self.tr( + "Could not find the reference for tag '{0}'. It either doesn't " + "exist, or the index is out of date. The index can be updated " + "from the Tools menu, or by pressing {1}." + ).format( + tag, "F9" + )) + else: if mode == nwDocMode.EDIT: self.openDocument(tHandle, sTitle=sTitle) elif mode == nwDocMode.VIEW: @@ -1293,19 +1299,3 @@ class GuiMain(QMainWindow): """Set the window title and add the project's name.""" self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName]))) return - - def _getTagSource(self, tag: str) -> tuple[str | None, str | None]: - """Handle the index lookup of a tag and display an alert if the - tag cannot be found. - """ - tHandle, sTitle = SHARED.project.index.getTagSource(tag) - if tHandle is None: - SHARED.error(self.tr( - "Could not find the reference for tag '{0}'. It either doesn't " - "exist, or the index is out of date. The index can be updated " - "from the Tools menu, or by pressing {1}." - ).format( - tag, "F9" - )) - return None, None - return tHandle, sTitle diff --git a/tests/conftest.py b/tests/conftest.py index 4ec3eeb3..ada43e28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,8 +54,10 @@ def resetConfigVars(): CONFIG.setBackupPath(_TMP_ROOT) CONFIG.setGuiFont(None) CONFIG.setTextFont(None) + CONFIG.backupOnClose = False CONFIG._homePath = _TMP_ROOT CONFIG._dLocale = QLocale("en_GB") + CONFIG.pdfDocs = _TMP_ROOT / "manual.pdf" CONFIG.guiLocale = "en_GB" return @@ -72,6 +74,7 @@ def sessionFixture(): shutil.rmtree(_TMP_ROOT) _TMP_ROOT.mkdir() _TMP_CONF.mkdir() + (_TMP_ROOT / "manual.pdf").touch() return @@ -161,6 +164,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture): monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) + assert nwGUI is not None qtbot.addWidget(nwGUI) resetConfigVars() nwGUI.docEditor.initEditor() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index eb87ac31..d7fb5d0d 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -20,19 +20,22 @@ along with this program. If not, see . """ from __future__ import annotations +import shutil import sys +from pathlib import Path from shutil import copyfile import pytest from PyQt5.QtCore import Qt from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QInputDialog, QMenu +from PyQt5.QtWidgets import QInputDialog, QMenu, QMessageBox from novelwriter import CONFIG, SHARED +from novelwriter.constants import nwFiles from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.enum import nwDocAction, nwFocus, nwItemType, nwView +from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView @@ -682,6 +685,12 @@ def testGuiMain_Viewing(qtbot, monkeypatch, nwGUI, projPath, mockRnd): def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test various features of the main window.""" buildTestProject(nwGUI, projPath) + cHandle = SHARED.project.newFile("Jane", C.hCharRoot) + newDoc = SHARED.project.storage.getDocument(cHandle) + newDoc.writeDocument("# Jane\n\n@tag: Jane\n\n") + nwGUI.projView.projTree.revealNewTreeItem(cHandle) + nwGUI.rebuildIndex(beQuiet=True) + assert SHARED.focusMode is False # Focus Mode @@ -721,6 +730,20 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert SHARED.focusMode is True nwGUI.closeDocument() assert SHARED.focusMode is False + nwGUI.openDocument(C.hSceneDoc) + + # Pressing Escape turns off focus mode + nwGUI.toggleFocusMode() + assert SHARED.focusMode is True + qtbot.keyClick(nwGUI, Qt.Key.Key_Escape) + assert SHARED.focusMode is False + + # If search is active, Escape is redirected to editor + nwGUI.toggleFocusMode() + assert SHARED.focusMode is True + nwGUI.docEditor.beginSearch() + qtbot.keyClick(nwGUI, Qt.Key.Key_Escape) + assert SHARED.focusMode is True # Full Screen Mode # ================ @@ -738,8 +761,25 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.sideBar.mSettings.show() nwGUI.sideBar.mSettings.hide() - # Document Open Errors - # ==================== + # Redirect Tag Open + # ================= + + nwGUI.closeDocument() + nwGUI.closeDocViewer() + assert nwGUI.docEditor.docHandle is None + assert nwGUI.docViewer.docHandle is None + nwGUI._followTag("John", nwDocMode.EDIT) # Doesn't exist + assert nwGUI.docEditor.docHandle is None + assert nwGUI.docViewer.docHandle is None + nwGUI._followTag("Jane", nwDocMode.EDIT) + assert nwGUI.docEditor.docHandle == cHandle + assert nwGUI.docViewer.docHandle is None + nwGUI._followTag("Jane", nwDocMode.VIEW) + assert nwGUI.docEditor.docHandle == cHandle + assert nwGUI.docViewer.docHandle == cHandle + + # Errors Handling + # =============== # Cannot edit a folder assert nwGUI.openDocument(C.hChapterDir) is False @@ -752,6 +792,57 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # qtbot.stop() +@pytest.mark.gui +def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd): + """Test various features of the main window.""" + buildTestProject(nwGUI, projPath) + nwGUI.openDocument(C.hSceneDoc) + nwGUI.viewDocument(C.hTitlePage) + + # Handle broken index on project open + nwGUI.closeProject() + idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE + assert idxPath.read_text() != "{}" + idxPath.write_text("{}") + assert idxPath.read_text() == "{}" + + nwGUI.openProject(projPath) + nwGUI.saveProject() + assert idxPath.read_text() != "{}" + assert nwGUI.docEditor.docHandle == C.hSceneDoc + assert nwGUI.docViewer.docHandle == C.hTitlePage + + # Block closing + assert SHARED.hasProject is True + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + assert nwGUI.openProject(projPath) is False + assert SHARED.hasProject is True + + # Don't open on lockfile question: No + lockPath: Path = projPath / nwFiles.PROJ_LOCK + lockBack: Path = projPath / f"{nwFiles.PROJ_LOCK}.bak" + + shutil.copyfile(lockPath, lockBack) + nwGUI.closeProject() + shutil.copyfile(lockBack, lockPath) + + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + assert nwGUI.openProject(projPath) is False + + assert nwGUI.openProject(projPath) is True + + # Backup on close + backDir = CONFIG.backupPath() / SHARED.project.data.name + assert not backDir.exists() + + CONFIG.backupOnClose = True + assert nwGUI.openProject(projPath) is True + nwGUI.closeProject() + assert len(list(backDir.glob("*.zip"))) == 1 + + @pytest.mark.gui def testGuiMain_FocusView(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test switching focus and view of the main window.""" From ca2b509d54b4d482f6ca610b19b52f6cd7ff7053 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:11:32 +0100 Subject: [PATCH 4/5] Make the test a little safer --- tests/test_gui/test_gui_guimain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index d7fb5d0d..622d81b6 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -840,7 +840,8 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) CONFIG.backupOnClose = True assert nwGUI.openProject(projPath) is True nwGUI.closeProject() - assert len(list(backDir.glob("*.zip"))) == 1 + assert backDir.exists() + assert len(list(backDir.iterdir())) > 0 @pytest.mark.gui From f2e36d9974c4450856e6859d87143cb1ae79ef15 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:18:16 +0100 Subject: [PATCH 5/5] Cover missing lines in error module --- tests/test_base/test_base_error.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_base/test_base_error.py b/tests/test_base/test_base_error.py index 4d9990f2..05aff792 100644 --- a/tests/test_base/test_base_error.py +++ b/tests/test_base/test_base_error.py @@ -20,6 +20,8 @@ along with this program. If not, see . """ from __future__ import annotations +import sys + import pytest from novelwriter.error import NWErrorMessage, exceptionHandler @@ -29,8 +31,7 @@ from tests.mocked import causeException @pytest.mark.base def testBaseError_Dialog(qtbot, monkeypatch, nwGUI): - """Test the error dialog. - """ + """Test the error dialog.""" nwErr = NWErrorMessage(nwGUI) qtbot.addWidget(nwErr) nwErr.show() @@ -57,6 +58,14 @@ def testBaseError_Dialog(qtbot, monkeypatch, nwGUI): assert message != "" assert "(Unknown)" in message + # No enchant version retrieved + with monkeypatch.context() as mp: + mp.setitem(sys.modules, "enchant", None) + nwErr.setMessage(Exception, "Almost Fine Error", None) # type: ignore + message = nwErr.msgBody.toPlainText() + assert message != "" + assert "enchant: Unknown" in message + nwErr._doClose() nwErr.close() nwGUI.closeMain()