From c4140d753c4315e0f81f7b68a4f78f4d3203e6fc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 13 Aug 2023 23:32:10 +0200 Subject: [PATCH] Improve handling of project closing --- novelwriter/core/project.py | 10 +- novelwriter/core/status.py | 12 +- novelwriter/gui/doceditor.py | 57 +++--- novelwriter/gui/docviewer.py | 255 +++++++++++++-------------- novelwriter/gui/itemdetails.py | 58 +++--- novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/noveltree.py | 9 +- novelwriter/gui/outline.py | 5 +- novelwriter/gui/projtree.py | 4 +- novelwriter/guimain.py | 51 ++---- novelwriter/shared.py | 19 +- tests/test_gui/test_gui_doceditor.py | 18 +- tests/test_gui/test_gui_docviewer.py | 12 +- tests/test_gui/test_gui_guimain.py | 16 +- tests/test_gui/test_gui_mainmenu.py | 12 +- tests/test_gui/test_gui_noveltree.py | 12 +- tests/test_gui/test_gui_outline.py | 4 +- tests/test_gui/test_gui_projtree.py | 12 +- 18 files changed, 281 insertions(+), 289 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 2164d4e2..27ac56af 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -382,9 +382,11 @@ class NWProject(QObject): self._storage.runPostSaveTasks(autoSave=autoSave) # Update recent projects - CONFIG.recentProjects.update( - self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime - ) + storePath = self._storage.storagePath + if storePath: + CONFIG.recentProjects.update( + storePath, self._data.name, sum(self._data.currCounts), saveTime + ) self._storage.writeLockFile() self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) @@ -393,7 +395,7 @@ class NWProject(QObject): return True def closeProject(self, idleTime: float = 0.0) -> None: - """Close the current project and clear all meta data.""" + """Close the project.""" logger.info("Closing project") self._options.saveSettings() self._tree.writeToCFile() diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index fad14027..80b0ab1c 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -129,7 +129,7 @@ class NWStatus: def name(self, key: str | None) -> str: """Return the name associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["name"] elif self._default is not None: return self._store[self._default]["name"] @@ -137,7 +137,7 @@ class NWStatus: def cols(self, key: str | None) -> tuple[int, int, int]: """Return the colours associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["cols"] elif self._default is not None: return self._store[self._default]["cols"] @@ -145,7 +145,7 @@ class NWStatus: def count(self, key: str | None) -> int: """Return the count associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["count"] elif self._default is not None: return self._store[self._default]["count"] @@ -153,7 +153,7 @@ class NWStatus: def icon(self, key: str | None) -> QIcon: """Return the icon associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["icon"] elif self._default is not None: return self._store[self._default]["icon"] @@ -186,9 +186,9 @@ class NWStatus: self._store[key]["count"] = 0 return - def increment(self, key: str) -> None: + def increment(self, key: str | None) -> None: """Increment the counter for a given entry.""" - if key in self._store: + if key and key in self._store: self._store[key]["count"] += 1 return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 0c4287b3..4caa855e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -101,7 +101,7 @@ class GuiDocEditor(QTextEdit): self._wordCount = 0 # Word count self._paraCount = 0 # Paragraph count self._lastEdit = 0 # Time stamp of last edit - self._lastActive = 0 # Time stamp of last activity + self._lastActive = 0.0 # Time stamp of last activity self._lastFind = None # Position of the last found search word self._bigDoc = False # Flag for very large document size self._doReplace = False # Switch to temporarily disable auto-replace @@ -188,6 +188,34 @@ class GuiDocEditor(QTextEdit): return + ## + # Properties + ## + + @property + def docChanged(self) -> bool: + """Return the changed status of the document.""" + return self._docChanged + + @property + def docHandle(self) -> str | None: + """Return the handle of the currently open document.""" + return self._docHandle + + @property + def lastActive(self) -> float: + """Return the last active timestamp for the user.""" + return self._lastActive + + @property + def isEmpty(self) -> bool: + """Check if the current document is empty.""" + return self.document().isEmpty() + + ## + # Methods + ## + def clearEditor(self): """Clear the current document and reset all document-related flags and counters. @@ -203,7 +231,7 @@ class GuiDocEditor(QTextEdit): self._wordCount = 0 self._paraCount = 0 self._lastEdit = 0 - self._lastActive = 0 + self._lastActive = 0.0 self._lastFind = None self._bigDoc = False self._doReplace = False @@ -580,31 +608,6 @@ class GuiDocEditor(QTextEdit): return - ## - # Properties - ## - - def docChanged(self): - """Return the changed status of the document in the editor. - """ - return self._docChanged - - def docHandle(self): - """Return the handle of the currently open document. Return - None if no document is open. - """ - return self._docHandle - - def lastActive(self): - """Return the last active timestamp for the user. - """ - return self._lastActive - - def isEmpty(self): - """Wrapper function to check if the current document is empty. - """ - return self.document().isEmpty() - ## # Getters ## diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 8c1b143f..d4e7ad81 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -30,10 +30,11 @@ from __future__ import annotations import logging from enum import Enum +from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal +from PyQt5.QtCore import QPoint, Qt, QUrl, QSize, pyqtSlot, pyqtSignal from PyQt5.QtGui import ( - QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor + QMouseEvent, QResizeEvent, QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor ) from PyQt5.QtWidgets import ( qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, @@ -46,6 +47,9 @@ from novelwriter.error import logException from novelwriter.constants import nwUnicode from novelwriter.core.tohtml import ToHtml +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -53,7 +57,7 @@ class GuiDocViewer(QTextBrowser): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiDocViewer") @@ -90,25 +94,43 @@ class GuiDocViewer(QTextBrowser): return - def clearViewer(self): - """Clear the content of the document and reset key variables. - """ + ## + # Properties + ## + + @property + def docHandle(self) -> str | None: + """Return the handle of the currently open document.""" + return self._docHandle + + @property + def scrollPosition(self) -> int: + """Return the scrollbar position.""" + vBar = self.verticalScrollBar() + if vBar.isVisible(): + return vBar.value() + return 0 + + ## + # Methods + ## + + def clearViewer(self) -> None: + """Clear the content of the document and reset key variables.""" self.clear() self.setSearchPaths([""]) self._docHandle = None self.docHeader.setTitleFromHandle(self._docHandle) - return True + return - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.docHeader.updateTheme() self.docFooter.updateTheme() return - def initViewer(self): - """Set editor settings from main config. - """ + def initViewer(self) -> None: + """Set editor settings from main config.""" self._makeStyleSheet() # Set Font @@ -157,11 +179,10 @@ class GuiDocViewer(QTextBrowser): if self._docHandle is not None: self.reloadText() - return True + return - def loadText(self, tHandle, updateHistory=True): - """Load text into the viewer from an item handle. - """ + def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: + """Load text into the viewer from an item handle.""" if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): logger.warning("Item not found") return False @@ -224,23 +245,20 @@ class GuiDocViewer(QTextBrowser): return True - def reloadText(self): - """Reload the text in the current document. - """ - self.loadText(self._docHandle, updateHistory=False) + def reloadText(self) -> None: + """Reload the text in the current document.""" + if self._docHandle: + self.loadText(self._docHandle, updateHistory=False) return - def redrawText(self): - """Redraw the text by marking the document content as "dirty". - """ + def redrawText(self) -> None: + """Redraw the text by marking the content as "dirty".""" self.document().markContentsDirty(0, self.document().characterCount()) self.updateDocMargins() return - def docAction(self, theAction): - """Wrapper function for various document actions on the current - document. - """ + def docAction(self, theAction: nwDocAction) -> bool: + """Process document actions on the current document.""" logger.debug("Requesting action: '%s'", theAction.name) if self._docHandle is None: logger.error("No document open") @@ -258,9 +276,8 @@ class GuiDocViewer(QTextBrowser): return False return True - def navigateTo(self, tAnchor): - """Go to a specific #link in the document. - """ + def navigateTo(self, tAnchor: str) -> bool: + """Go to a specific #link in the document.""" if not isinstance(tAnchor, str): return False if tAnchor.startswith("#"): @@ -268,27 +285,13 @@ class GuiDocViewer(QTextBrowser): self.setSource(QUrl(tAnchor)) return True - def navBackward(self): - """Navigate backwards in the document view history. - """ - self.docHistory.backward() - return - - def navForward(self): - """Navigate forwards in the document view history. - """ - self.docHistory.forward() - return - - def clearNavHistory(self): - """Clear the navigation history. - """ + def clearNavHistory(self) -> None: + """Clear the navigation history.""" self.docHistory.clear() return - def updateDocMargins(self): - """Automatically adjust the margins so the text is centred. - """ + def updateDocMargins(self) -> None: + """Automatically adjust the margins so the text is centred.""" wW = self.width() wH = self.height() cM = CONFIG.getTextMargin() @@ -320,41 +323,20 @@ class GuiDocViewer(QTextBrowser): # Setters ## - def setScrollPosition(self, thePos): - """Set the scrollbar position. - """ + def setScrollPosition(self, pos: int) -> None: + """Set the scrollbar position.""" vBar = self.verticalScrollBar() if vBar.isVisible(): - vBar.setValue(thePos) + vBar.setValue(pos) return - ## - # Getters - ## - - def docHandle(self): - """Return the handle of the currently open document. Returns - None if no document is open. - """ - return self._docHandle - - def getScrollPosition(self): - """Get the scrollbar position. Returns 0 if no scrollbar. - """ - vBar = self.verticalScrollBar() - if vBar.isVisible(): - return vBar.value() - return 0 - ## # Public Slots ## @pyqtSlot(str) - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ + def updateDocInfo(self, tHandle: str) -> None: + """Update the header titlebar if needed.""" if tHandle == self._docHandle: self.docHeader.setTitleFromHandle(self._docHandle) self.updateDocMargins() @@ -364,11 +346,22 @@ class GuiDocViewer(QTextBrowser): # Private Slots ## + @pyqtSlot() + def navBackward(self) -> None: + """Navigate backwards in the document view history.""" + self.docHistory.backward() + return + + @pyqtSlot() + def navForward(self) -> None: + """Navigate forwards in the document view history.""" + self.docHistory.forward() + return + @pyqtSlot("QUrl") - def _linkClicked(self, theURL): - """Process a clicked link internally in the document. - """ - theLink = theURL.url() + def _linkClicked(self, url: QUrl) -> None: + """Process a clicked link internally in the document.""" + theLink = url.url() logger.debug("Clicked link: '%s'", theLink) if len(theLink) > 0: theBits = theLink.split("=") @@ -377,9 +370,8 @@ class GuiDocViewer(QTextBrowser): return @pyqtSlot("QPoint") - def _openContextMenu(self, thePos): - """Triggered by right click to open the context menu. - """ + def _openContextMenu(self, point: QPoint) -> None: + """Open context menu at location.""" userCursor = self.textCursor() userSelection = userCursor.hasSelection() @@ -404,18 +396,18 @@ class GuiDocViewer(QTextBrowser): mnuSelWord = QAction(self.tr("Select Word"), mnuContext) mnuSelWord.triggered.connect( - lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos) + lambda: self._makePosSelection(QTextCursor.WordUnderCursor, point) ) mnuContext.addAction(mnuSelWord) mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext) mnuSelPara.triggered.connect( - lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos) + lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, point) ) mnuContext.addAction(mnuSelPara) # Open the context menu - mnuContext.exec_(self.viewport().mapToGlobal(thePos)) + mnuContext.exec_(self.viewport().mapToGlobal(point)) return @@ -423,37 +415,33 @@ class GuiDocViewer(QTextBrowser): # Events ## - def resizeEvent(self, theEvent): - """If the text editor is resized, we must make sure the document - has its margins adjusted according to user preferences. - """ + def resizeEvent(self, event: QResizeEvent) -> None: + """Update document margins when widget is resized.""" self.updateDocMargins() - super().resizeEvent(theEvent) + super().resizeEvent(event) return - def mouseReleaseEvent(self, theEvent): - """Capture mouse click events on the document. - """ - if theEvent.button() == Qt.BackButton: + def mouseReleaseEvent(self, event: QMouseEvent) -> None: + """Capture mouse click events on the document.""" + if event.button() == Qt.BackButton: self.navBackward() - elif theEvent.button() == Qt.ForwardButton: + elif event.button() == Qt.ForwardButton: self.navForward() else: - super().mouseReleaseEvent(theEvent) + super().mouseReleaseEvent(event) return ## # Internal Functions ## - def _makeSelection(self, selMode): - """Wrapper function to select text based on a selection mode. - """ + def _makeSelection(self, selType: QTextCursor.SelectionType) -> None: + """Handle select of text based on a selection mode.""" theCursor = self.textCursor() theCursor.clearSelection() - theCursor.select(selMode) + theCursor.select(selType) - if selMode == QTextCursor.BlockUnderCursor: + if selType == QTextCursor.BlockUnderCursor: # This selection mode also selects the preceding paragraph # separator, which we want to avoid. posS = theCursor.selectionStart() @@ -467,19 +455,18 @@ class GuiDocViewer(QTextBrowser): return - def _makePosSelection(self, selMode, thePos): - """Wrapper function to select text based on selection mode, but - first move cursor to given position. - """ - theCursor = self.cursorForPosition(thePos) + def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None: + """Handle text selection at a given location.""" + theCursor = self.cursorForPosition(pos) self.setTextCursor(theCursor) - self._makeSelection(selMode) + self._makeSelection(selType) return - def _makeStyleSheet(self): + def _makeStyleSheet(self) -> None: """Generate an appropriate style sheet for the document viewer, based on the current syntax highlighter theme, """ + pTheme = SHARED.theme styleSheet = ( "body {{" " color: rgb({tColR}, {tColG}, {tColB});" @@ -506,31 +493,31 @@ class GuiDocViewer(QTextBrowser): " text-align: center;" "}}\n" ).format( - tColR=SHARED.theme.colText[0], - tColG=SHARED.theme.colText[1], - tColB=SHARED.theme.colText[2], - hColR=SHARED.theme.colHead[0], - hColG=SHARED.theme.colHead[1], - hColB=SHARED.theme.colHead[2], - aColR=SHARED.theme.colVal[0], - aColG=SHARED.theme.colVal[1], - aColB=SHARED.theme.colVal[2], - eColR=SHARED.theme.colEmph[0], - eColG=SHARED.theme.colEmph[1], - eColB=SHARED.theme.colEmph[2], - kColR=SHARED.theme.colKey[0], - kColG=SHARED.theme.colKey[1], - kColB=SHARED.theme.colKey[2], - cColR=SHARED.theme.colHidden[0], - cColG=SHARED.theme.colHidden[1], - cColB=SHARED.theme.colHidden[2], - mColR=SHARED.theme.colMod[0], - mColG=SHARED.theme.colMod[1], - mColB=SHARED.theme.colMod[2], + tColR=pTheme.colText[0], + tColG=pTheme.colText[1], + tColB=pTheme.colText[2], + hColR=pTheme.colHead[0], + hColG=pTheme.colHead[1], + hColB=pTheme.colHead[2], + aColR=pTheme.colVal[0], + aColG=pTheme.colVal[1], + aColB=pTheme.colVal[2], + eColR=pTheme.colEmph[0], + eColG=pTheme.colEmph[1], + eColB=pTheme.colEmph[2], + kColR=pTheme.colKey[0], + kColG=pTheme.colKey[1], + kColB=pTheme.colKey[2], + cColR=pTheme.colHidden[0], + cColG=pTheme.colHidden[1], + cColB=pTheme.colHidden[2], + mColR=pTheme.colMod[0], + mColG=pTheme.colMod[1], + mColB=pTheme.colMod[2], ) self.document().setDefaultStyleSheet(styleSheet) - return True + return # END Class GuiDocViewer @@ -628,7 +615,7 @@ class GuiDocViewHistory: """Update the scrollbar position of the previous entry. """ if self._prevPos >= 0 and self._prevPos < len(self._posHistory): - self._posHistory[self._prevPos] = self.docViewer.getScrollPosition() + self._posHistory[self._prevPos] = self.docViewer.scrollPosition return def _updateNavButtons(self): @@ -864,7 +851,7 @@ class GuiDocViewHeader(QWidget): def _refreshDocument(self): """Reload the content of the document. """ - if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle(): + if self.docViewer.docHandle == self.mainGui.docEditor.docHandle: self.mainGui.saveDocument() self.docViewer.reloadText() return @@ -1102,8 +1089,8 @@ class GuiDocViewFooter(QWidget): """ logger.debug("Reference sticky is %s", str(theState)) self.docViewer.stickyRef = theState - if not theState and self.docViewer.docHandle() is not None: - self.viewMeta.refreshReferences(self.docViewer.docHandle()) + if not theState and self.docViewer.docHandle is not None: + self.viewMeta.refreshReferences(self.docViewer.docHandle) return @pyqtSlot(bool) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 24c1fa93..bf12ec50 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -25,19 +25,24 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING + +from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel from novelwriter import CONFIG, SHARED from novelwriter.constants import trConst, nwLabels +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) class GuiItemDetails(QWidget): - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiItemDetails") @@ -187,35 +192,28 @@ class GuiItemDetails(QWidget): # Class Methods ## - def clearDetails(self): - """Clear all the data values. - """ + def clearDetails(self) -> None: + """Clear all the data values.""" self._itemHandle = None - - self.labelIcon.setPixmap(QPixmap(1, 1)) - self.statusIcon.setPixmap(QPixmap(1, 1)) - self.classIcon.setText("") - self.usageIcon.setText("") - - self.labelData.setText("–") - self.statusData.setText("–") - self.classData.setText("–") - self.usageData.setText("–") - - self.cCountData.setText("–") - self.wCountData.setText("–") - self.pCountData.setText("–") - + self.labelIcon.clear() + self.labelData.clear() + self.statusIcon.clear() + self.statusData.clear() + self.classIcon.clear() + self.classData.clear() + self.usageIcon.clear() + self.usageData.clear() + self.cCountData.clear() + self.wCountData.clear() + self.pCountData.clear() return - def refreshDetails(self): - """Reload the content of the details panel. - """ + def refreshDetails(self) -> None: + """Reload the content of the details panel.""" self.updateViewBox(self._itemHandle) - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.updateViewBox(self._itemHandle) return @@ -224,9 +222,8 @@ class GuiItemDetails(QWidget): ## @pyqtSlot(str) - def updateViewBox(self, tHandle): - """Populate the details box from a given handle. - """ + def updateViewBox(self, tHandle: str) -> None: + """Populate the details box from a given handle.""" if tHandle is None: self.clearDetails() return @@ -294,7 +291,7 @@ class GuiItemDetails(QWidget): return @pyqtSlot(str, int, int, int) - def updateCounts(self, tHandle, cC, wC, pC): + def updateCounts(self, tHandle: str, cC: int, wC: int, pC: int) -> None: """Update the counts if the handle is the same as the one we're already showing. Otherwise, do nothing. """ @@ -302,7 +299,6 @@ class GuiItemDetails(QWidget): self.cCountData.setText(f"{cC:n}") self.wCountData.setText(f"{wC:n}") self.pCountData.setText(f"{pC:n}") - return # END Class GuiItemDetails diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 21aa8bc3..b12f655f 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -349,13 +349,13 @@ class GuiMainMenu(QMenuBar): # View > Go Backward self.aViewPrev = QAction(self.tr("Navigate Backward"), self) self.aViewPrev.setShortcut("Alt+Left") - self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward()) + self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward) self.viewMenu.addAction(self.aViewPrev) # View > Go Forward self.aViewNext = QAction(self.tr("Navigate Forward"), self) self.aViewNext.setShortcut("Alt+Right") - self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward()) + self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward) self.viewMenu.addAction(self.aViewNext) # View > Separator diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 67bd8a5c..4d804c40 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -106,7 +106,7 @@ class GuiNovelView(QWidget): self.novelTree.initSettings() return - def clearProject(self): + def clearNovelView(self): """Clear project-related GUI content. """ self.novelTree.clearContent() @@ -130,8 +130,7 @@ class GuiNovelView(QWidget): "GuiNovelView", "lastColSize", 25 ) - self.clearProject() - + self.clearNovelView() self.novelBar.buildNovelRootMenu() self.novelBar.setLastColType(lastCol, doRefresh=False) self.novelBar.setCurrentRoot(lastNovel) @@ -142,13 +141,13 @@ class GuiNovelView(QWidget): return def closeProjectTasks(self): - """Run closing project tasks. - """ + """Run closing project tasks.""" lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize pOptions = SHARED.project.options pOptions.setValue("GuiNovelView", "lastCol", lastColType) pOptions.setValue("GuiNovelView", "lastColSize", lastColSize) + self.clearNovelView() return def setTreeFocus(self): diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index d61adad8..4fd27176 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -118,7 +118,7 @@ class GuiOutlineView(QWidget): self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline")) return - def clearProject(self): + def clearOutline(self): """Clear project-related GUI content. """ self.outlineData.clearDetails() @@ -134,7 +134,7 @@ class GuiOutlineView(QWidget): logger.debug("Setting outline tree to root item '%s'", lastOutline) - self.clearProject() + self.clearOutline() self.outlineBar.populateNovelList() self.outlineBar.setCurrentRoot(lastOutline) self.outlineBar.setEnabled(True) @@ -144,6 +144,7 @@ class GuiOutlineView(QWidget): def closeProjectTasks(self): self.outlineTree.closeProjectTasks() self.outlineData.updateClasses() + self.clearOutline() return def splitSizes(self): diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 43b26631..52235374 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -165,7 +165,7 @@ class GuiProjectView(QWidget): self.projTree.initSettings() return - def clearProject(self) -> None: + def clearProjectView(self) -> None: """Clear project-related GUI content.""" self.projBar.clearContent() self.projBar.setEnabled(False) @@ -964,7 +964,7 @@ class GuiProjectTree(QTreeWidget): trItemP.takeChild(tIndex) for dHandle in reversed(self.getTreeFromHandle(tHandle)): - if self.mainGui.docEditor.docHandle() == dHandle: + if self.mainGui.docEditor.docHandle == dHandle: self.mainGui.closeDocument() SHARED.project.removeItem(dHandle) self._treeMap.pop(dHandle, None) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index f7f2ffdd..7eab0b8f 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -236,7 +236,7 @@ class GuiMain(QMainWindow): # Connect Signals # =============== - SHARED.project.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) + SHARED.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) self.viewsBar.viewChangeRequested.connect(self._changeView) @@ -337,25 +337,6 @@ class GuiMain(QMainWindow): return - def clearGUI(self) -> None: - """Clear all sub-elements of the main GUI.""" - # Project Area - self.projView.clearProject() - self.novelView.clearProject() - self.itemDetails.clearDetails() - - # Work Area - self.docEditor.clearEditor() - self.docEditor.setDictionaries() - self.closeDocViewer(byUser=False) - self.outlineView.clearProject() - - # General - self.mainStatus.clearStatus() - self._updateWindowTitle() - - return - def initMain(self) -> None: """Initialise elements that depend on user settings.""" self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000)) @@ -446,7 +427,7 @@ class GuiMain(QMainWindow): if not msgYes: return False - if self.docEditor.docChanged(): + if self.docEditor.docChanged: self.saveDocument() saveOK = self.saveProject() @@ -462,14 +443,20 @@ class GuiMain(QMainWindow): if saveOK: self.closeDocument() self.docViewer.clearNavHistory() + self.closeDocViewer(byUser=False) + self.outlineView.closeProjectTasks() self.novelView.closeProjectTasks() + self.projView.clearProjectView() + self.itemDetails.clearDetails() + self.mainStatus.clearStatus() SHARED.closeProject(self.idleTime) self.idleRefTime = time() self.idleTime = 0.0 - self.clearGUI() + self.docEditor.setDictionaries() + self._updateWindowTitle() self._changeView(nwView.PROJECT) return saveOK @@ -594,7 +581,7 @@ class GuiMain(QMainWindow): self.toggleFocusMode() self.docEditor.saveCursorPosition() - if self.docEditor.docChanged(): + if self.docEditor.docChanged: self.saveDocument() self.docEditor.clearEditor() if not beforeOpen: @@ -614,7 +601,7 @@ class GuiMain(QMainWindow): return False self._changeView(nwView.EDITOR) - cHandle = self.docEditor.docHandle() + cHandle = self.docEditor.docHandle if cHandle == tHandle: self.docEditor.setCursorLine(tLine) if changeFocus: @@ -682,7 +669,7 @@ class GuiMain(QMainWindow): logger.debug("Viewing document, but no handle provided") if self.docEditor.hasFocus(): - tHandle = self.docEditor.docHandle() + tHandle = self.docEditor.docHandle if tHandle is not None: self.saveDocument() @@ -750,13 +737,13 @@ class GuiMain(QMainWindow): ), level=nwAlert.ERROR, exc=exc) return False - if self.docEditor.docHandle() is None: + if self.docEditor.docHandle is None: self.makeAlert(self.tr( "Please open a document to import the text file into." ), level=nwAlert.ERROR) return False - if not self.docEditor.isEmpty(): + if not self.docEditor.isEmpty: msgYes = self.askQuestion(self.tr( "Importing the file will overwrite the current content of " "the document. Do you want to proceed?" @@ -825,7 +812,7 @@ class GuiMain(QMainWindow): return False if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): - tHandle = self.docEditor.docHandle() + tHandle = self.docEditor.docHandle self.projView.renameTreeItem(tHandle) return True @@ -1219,7 +1206,7 @@ class GuiMain(QMainWindow): """Handle toggle focus mode. The Main GUI Focus Mode hides tree, view, statusbar and menu. """ - if self.docEditor.docHandle() is None: + if self.docEditor.docHandle is None: logger.error("No document open, so not activating Focus Mode") return False @@ -1242,7 +1229,7 @@ class GuiMain(QMainWindow): if self.splitView.isVisible(): self.splitView.setVisible(False) - elif self.docViewer.docHandle() is not None: + elif self.docViewer.docHandle is not None: self.splitView.setVisible(True) return True @@ -1474,7 +1461,7 @@ class GuiMain(QMainWindow): return currTime = time() - editIdle = currTime - self.docEditor.lastActive() > CONFIG.userIdleTime + editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime userIdle = qApp.applicationState() != Qt.ApplicationActive if editIdle or userIdle: @@ -1502,7 +1489,7 @@ class GuiMain(QMainWindow): @pyqtSlot() def _autoSaveDocument(self) -> None: """Autosave of the document. This is a timer-activated slot.""" - if SHARED.hasProject and self.docEditor.docChanged(): + if SHARED.hasProject and self.docEditor.docChanged: logger.debug("Autosaving document") self.saveDocument() return diff --git a/novelwriter/shared.py b/novelwriter/shared.py index d05b2ce5..48cb5eb7 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -28,6 +28,8 @@ import logging from typing import TYPE_CHECKING from pathlib import Path +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot + if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain from novelwriter.gui.theme import GuiTheme @@ -36,9 +38,12 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) -class SharedData: +class SharedData(QObject): + + projectStatusChanged = pyqtSignal(bool) def __init__(self) -> None: + super().__init__() self._gui = None self._theme = None self._project = None @@ -121,6 +126,16 @@ class SharedData: """Remove the project lock.""" return self.project.storage.clearLockFile() + ## + # Internal Slots + ## + + @pyqtSlot(bool) + def _processProjectStatusChange(self, state: bool) -> None: + """Forward the project status slot.""" + self.projectStatusChanged.emit(state) + return + ## # Internal Functions ## @@ -129,8 +144,10 @@ class SharedData: """Create a new project instance.""" from novelwriter.core.project import NWProject if isinstance(self._project, NWProject): + self._project.projectStatusChanged.disconnect() self._project.deleteLater() self._project = NWProject(self.mainGui) + self._project.projectStatusChanged.connect(self._processProjectStatusChange) return # END Class SharedData diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 8452623f..1096f8e2 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -194,10 +194,10 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.docEditor.getText() == "### New Scene\n\nSome\ntext.\nMore\u00a0text.\n" # Check Propertoes - assert nwGUI.docEditor.docChanged() is True - assert nwGUI.docEditor.docHandle() == C.hSceneDoc - assert nwGUI.docEditor.lastActive() > 0.0 - assert nwGUI.docEditor.isEmpty() is False + assert nwGUI.docEditor.docChanged is True + assert nwGUI.docEditor.docHandle == C.hSceneDoc + assert nwGUI.docEditor.lastActive > 0.0 + assert nwGUI.docEditor.isEmpty is False # Cursor Position assert nwGUI.docEditor.setCursorPosition(None) is False @@ -1361,7 +1361,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Next match nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() == "2426c6f0ca922" # Next document + assert nwGUI.docEditor.docHandle == "2426c6f0ca922" # Next document nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) @@ -1371,11 +1371,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert nwGUI.docEditor.docSearch.doNextFile is True assert nwGUI.docEditor.docSearch.setSearchText("abcdef") nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() != "2426c6f0ca922" - assert nwGUI.docEditor.docHandle() == "04468803b92e1" + assert nwGUI.docEditor.docHandle != "2426c6f0ca922" + assert nwGUI.docEditor.docHandle == "04468803b92e1" nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() != "04468803b92e1" - assert nwGUI.docEditor.docHandle() == "7a992350f3eb6" + assert nwGUI.docEditor.docHandle != "04468803b92e1" + assert nwGUI.docEditor.docHandle == "7a992350f3eb6" # Toggle Replace nwGUI.docEditor.beginReplace() diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 69b11039..de21171f 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -50,7 +50,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8") theRect = nwGUI.projView.projTree.visualItemRect(theItem) qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) - assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" + assert nwGUI.docViewer.docHandle == "88243afbe5ed8" # Reload the text origText = nwGUI.docViewer.toPlainText() @@ -97,7 +97,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): # Close document nwGUI.docViewer.docHeader._closeDocument() - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None # Action on no document assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False @@ -114,17 +114,17 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): theRect = nwGUI.docViewer.cursorRect() # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) nwGUI.docViewer._linkClicked(QUrl("#char=Bod")) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Click mouse nav buttons qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100) - assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" + assert nwGUI.docViewer.docHandle == "88243afbe5ed8" qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center(), delay=100) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Scroll bar default on empty document nwGUI.docViewer.clear() - assert nwGUI.docViewer.getScrollPosition() == 0 + assert nwGUI.docViewer.scrollPosition == 0 nwGUI.docViewer.reloadText() # Change document title diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index e87ed70d..5ca394cd 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -153,10 +153,10 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # Novel Tree has focus @@ -164,11 +164,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None selItem = nwGUI.novelView.novelTree.topLevelItem(2) nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # Project Outline has focus @@ -176,11 +176,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None selItem = nwGUI.outlineView.outlineTree.topLevelItem(2) nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # qtbot.stop() @@ -506,9 +506,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): nwGUI.docEditor.wCounterDoc.run() # Save the document - assert nwGUI.docEditor.docChanged() + assert nwGUI.docEditor.docChanged assert nwGUI.saveDocument() - assert not nwGUI.docEditor.docChanged() + assert not nwGUI.docEditor.docChanged nwGUI.rebuildIndex() # Open and view the edited document diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index c3cf85f7..f0215f41 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -206,7 +206,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): # Clear the Text nwGUI.docEditor.clear() - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty # Alignment & Indent # ================== @@ -403,17 +403,17 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum): # Navigation History assert nwGUI.viewDocument("04468803b92e1") - assert nwGUI.docViewer.docHandle() == "04468803b92e1" + assert nwGUI.docViewer.docHandle == "04468803b92e1" assert nwGUI.docViewer.docHeader.backButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() qtbot.mouseClick(nwGUI.docViewer.docHeader.backButton, Qt.LeftButton) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" assert not nwGUI.docViewer.docHeader.backButton.isEnabled() assert nwGUI.docViewer.docHeader.forwardButton.isEnabled() qtbot.mouseClick(nwGUI.docViewer.docHeader.forwardButton, Qt.LeftButton) - assert nwGUI.docViewer.docHandle() == "04468803b92e1" + assert nwGUI.docViewer.docHandle == "04468803b92e1" assert nwGUI.docViewer.docHeader.backButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() @@ -438,10 +438,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): nwGUI.docEditor.clear() assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty assert nwGUI.docEditor.insertText(None) is False - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty # qtbot.stop() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 86675cf8..ab2812ad 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -118,26 +118,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Double-click item scItem.setSelected(True) assert scItem.isSelected() - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None novelTree._treeDoubleClick(scItem, 0) - assert nwGUI.docEditor.docHandle() == C.hSceneDoc + assert nwGUI.docEditor.docHandle == C.hSceneDoc # Open item with middle mouse button scItem.setSelected(True) assert scItem.isSelected() - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10) - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None scRect = novelTree.visualItemRect(scItem) oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE) scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() == C.hSceneDoc + assert nwGUI.docViewer.docHandle == C.hSceneDoc # Last Column # =========== diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index b1bb74f8..4d617b28 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -246,7 +246,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" outlineView._tagClicked("Bod") - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Scene One, Section Two selItem = outlineTree.topLevelItem(5) @@ -262,7 +262,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): assert outlineData.itemValue.text() == "Finished" outlineTree._treeDoubleClick(selItem, 0) - assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" + assert nwGUI.docEditor.docHandle == "88243afbe5ed8" # qtbot.stop() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c5428506..83705388 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -450,10 +450,10 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro # Deleting file is OK, and if it is open, it should close assert nwGUI.openDocument(C.hTitlePage) is True - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage assert projTree.permDeleteItem(C.hTitlePage) is True assert C.hTitlePage not in theProject.tree - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # Deleting folder + files recursively is ok assert projTree.permDeleteItem(C.hChapterDir) is True @@ -969,25 +969,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) # Try to open a file with nothings selected projTree.clearSelection() projTree._treeDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # When the item cannot be found projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore with monkeypatch.context() as mp: mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None) projTree._treeDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # Successfully open a file projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0) - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore # A non-file item should be expanded instead projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1) - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore # Navigate the Tree