From 78e194d8c16f7471abd8ca68203015cff01c5fb7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 2 May 2024 21:29:07 +0200 Subject: [PATCH 1/4] Improve handling of tool dialogs on project close --- novelwriter/extensions/modified.py | 6 +++++- novelwriter/shared.py | 13 +++++-------- novelwriter/tools/manuscript.py | 6 +++++- novelwriter/tools/manussettings.py | 7 ++++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 8f185b24..450647ac 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -28,6 +28,7 @@ along with this program. If not, see . from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QWheelEvent @@ -38,10 +39,13 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + class NToolDialog(QDialog): - def __init__(self, parent: QWidget | None = None) -> None: + def __init__(self, parent: GuiMain) -> None: super().__init__(parent=parent) self.setModal(False) if CONFIG.osDarwin: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index be323386..dced61a6 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -198,7 +198,7 @@ class SharedData(QObject): def closeProject(self) -> None: """Close the current project.""" - self._closeDialogs() + self._closeToolDialogs() self.project.closeProject(self._idleTime) self._resetProject() self._resetIdleTimer() @@ -357,15 +357,12 @@ class SharedData(QObject): self._idleTime = 0.0 return - def _closeDialogs(self) -> None: - """Close non-modal dialogs.""" - from novelwriter.tools.manuscript import GuiManuscript - from novelwriter.tools.writingstats import GuiWritingStats - + def _closeToolDialogs(self) -> None: + """Close all open tool dialogs.""" + from novelwriter.extensions.modified import NToolDialog for widget in self.mainGui.children(): - if isinstance(widget, (GuiManuscript, GuiWritingStats)): + if isinstance(widget, NToolDialog): widget.close() - return # END Class SharedData diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 58c4ec53..15ff0d30 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -28,6 +28,7 @@ import logging from datetime import datetime from time import time +from typing import TYPE_CHECKING from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent @@ -56,6 +57,9 @@ from novelwriter.types import ( QtSizeExpanding, QtSizeIgnored, QtUserRole ) +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -69,7 +73,7 @@ class GuiManuscript(NToolDialog): D_KEY = QtUserRole - def __init__(self, parent: QWidget) -> None: + def __init__(self, parent: GuiMain) -> None: super().__init__(parent=parent) logger.debug("Create: GuiManuscript") diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index e138b592..77b8035a 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -25,6 +25,8 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING + from PyQt5.QtCore import QEvent, pyqtSignal, pyqtSlot from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument from PyQt5.QtWidgets import ( @@ -51,6 +53,9 @@ from novelwriter.types import ( QtRoleApply, QtRoleReject, QtUserRole ) +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -69,7 +74,7 @@ class GuiBuildSettings(NToolDialog): newSettingsReady = pyqtSignal(BuildSettings) - def __init__(self, parent: QWidget, build: BuildSettings) -> None: + def __init__(self, parent: GuiMain, build: BuildSettings) -> None: super().__init__(parent=parent) logger.debug("Create: GuiBuildSettings") From e75b5e012c0db19ee1e3a6a6a015e4bdca5f9180 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 2 May 2024 21:45:50 +0200 Subject: [PATCH 2/4] Refactor how editor document is saved, and always save cursor position --- novelwriter/gui/doceditor.py | 1 - novelwriter/gui/projtree.py | 14 ++++++-------- novelwriter/gui/search.py | 2 +- novelwriter/guimain.py | 17 +++++++---------- novelwriter/shared.py | 5 +++++ novelwriter/tools/manusbuild.py | 2 +- novelwriter/tools/manuscript.py | 2 +- sample/nwProject.nwx | 6 +++--- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 7ed3fab7..97b336c1 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -477,7 +477,6 @@ class GuiDocEditor(QPlainTextEdit): cC, wC, pC = standardCounter(docText) self._updateDocCounts(cC, wC, pC) - self.saveCursorPosition() if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument.hashError: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 8074325b..ddb03f5e 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,10 +32,8 @@ from enum import Enum from time import time from typing import TYPE_CHECKING -from PyQt5.QtCore import QPoint, QTimer, Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import ( - QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette -) +from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette from PyQt5.QtWidgets import ( QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QShortcut, QTreeWidget, QTreeWidgetItem, QVBoxLayout, @@ -44,19 +42,19 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import minmax -from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels +from novelwriter.constants import nwHeaders, nwLabels, nwUnicode, trConst from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter from novelwriter.core.item import NWItem from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projectsettings import GuiProjectSettings -from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON from novelwriter.types import ( QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtSizeExpanding, - QtUserRole, + QtUserRole ) if TYPE_CHECKING: # pragma: no cover @@ -1424,7 +1422,7 @@ class GuiProjectTree(QTreeWidget): return False # Save the open document first, in case it's part of merge - self.mainGui.saveDocument() + SHARED.saveDocument() # Create merge object, and append docs docMerger = DocMerger(SHARED.project) diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index 93ee759b..17d18f53 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -259,7 +259,7 @@ class GuiProjectSearch(QWidget): if not self._blocked: QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) start = time() - SHARED.mainGui.saveDocument() + SHARED.saveDocument() self._blocked = True self._map = {} self.searchResult.clear() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index afe7966d..533de19e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -369,9 +369,7 @@ class GuiMain(QMainWindow): if not msgYes: return False - if self.docEditor.docChanged: - self.saveDocument() - + self.saveDocument() saveOK = self.saveProject() doBackup = False if SHARED.project.data.doBackup and CONFIG.backupOnClose: @@ -514,9 +512,7 @@ class GuiMain(QMainWindow): # Disable focus mode if it is active if SHARED.focusMode: SHARED.setFocusMode(False) - self.docEditor.saveCursorPosition() - if self.docEditor.docChanged: - self.saveDocument() + self.saveDocument() self.docEditor.clearEditor() if not beforeOpen: self.novelView.setActiveHandle(None) @@ -587,7 +583,8 @@ class GuiMain(QMainWindow): @pyqtSlot() def saveDocument(self) -> None: """Save the current documents.""" - if SHARED.hasProject: + self.docEditor.saveCursorPosition() + if SHARED.hasProject and self.docEditor.docChanged: self.docEditor.saveText() return @@ -1133,7 +1130,7 @@ class GuiMain(QMainWindow): @pyqtSlot() def _reloadViewer(self) -> None: """Reload the document in the viewer.""" - if self.docEditor.docChanged and self.docEditor.docHandle == self.docViewer.docHandle: + if self.docEditor.docHandle == self.docViewer.docHandle: # If the two panels have the same document, save any changes in the editor self.saveDocument() self.docViewer.reloadText() @@ -1213,7 +1210,7 @@ class GuiMain(QMainWindow): doSave &= SHARED.project.projChanged doSave &= SHARED.project.storage.isOpen() if doSave: - logger.debug("Autosaving project") + logger.debug("Auto-saving project") self.saveProject(autoSave=True) return @@ -1221,7 +1218,7 @@ class GuiMain(QMainWindow): def _autoSaveDocument(self) -> None: """Autosave of the document. This is a timer-activated slot.""" if SHARED.hasProject and self.docEditor.docChanged: - logger.debug("Autosaving document") + logger.debug("Auto-saving document") self.saveDocument() return diff --git a/novelwriter/shared.py b/novelwriter/shared.py index dced61a6..40d08c1f 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -171,6 +171,11 @@ class SharedData(QObject): logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount()) return + def saveDocument(self) -> None: + """Forward save document call to main GUI.""" + self.mainGui.saveDocument() + return + def openProject(self, path: str | Path, clearLock: bool = False) -> bool: """Open a project.""" if self.project.isValid: diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index e27e8b6a..8b6cdb0e 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -327,7 +327,7 @@ class GuiManuscriptBuild(QDialog): return False # Make sure editor content is saved before we start - SHARED.mainGui.saveDocument() + SHARED.saveDocument() docBuild = NWBuildDocument(SHARED.project, self._build) docBuild.queueAll() diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 15ff0d30..47bbdc10 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -339,7 +339,7 @@ class GuiManuscript(NToolDialog): return # Make sure editor content is saved before we start - SHARED.mainGui.saveDocument() + SHARED.saveDocument() docBuild = NWBuildDocument(SHARED.project, build) docBuild.setPreviewMode(True) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 40d46cee..5aa8cdba 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -58,7 +58,7 @@ Chapter One - + Making a Scene From 2b8338f5373913571e8fe13951a054dab6a8673d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 2 May 2024 22:13:35 +0200 Subject: [PATCH 3/4] Clean up Main GUI inheritance --- novelwriter/dialogs/wordlist.py | 10 ++---- novelwriter/gui/doceditor.py | 21 +++++-------- novelwriter/gui/noveltree.py | 16 +++------- novelwriter/gui/projtree.py | 22 +++++--------- novelwriter/gui/sidebar.py | 4 +-- novelwriter/gui/statusbar.py | 14 ++++----- novelwriter/guimain.py | 49 ++++++++++++++---------------- novelwriter/shared.py | 6 ++++ novelwriter/tools/writingstats.py | 4 +-- tests/test_gui/test_gui_guimain.py | 18 +++++------ 10 files changed, 68 insertions(+), 96 deletions(-) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 54ec2f2f..721ab91e 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -25,14 +25,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( QAbstractItemView, QApplication, QDialog, QDialogButtonBox, QFileDialog, - QHBoxLayout, QLineEdit, QListWidget, QVBoxLayout + QHBoxLayout, QLineEdit, QListWidget, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -42,9 +41,6 @@ from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.modified import NIconToolButton from novelwriter.types import QtDialogClose, QtDialogSave -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -52,8 +48,8 @@ class GuiWordList(QDialog): newWordListReady = pyqtSignal() - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiWordList") self.setObjectName("GuiWordList") diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 97b336c1..662b1c35 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -36,7 +36,6 @@ import logging from enum import Enum from time import time -from typing import TYPE_CHECKING from PyQt5.QtCore import ( QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, @@ -69,9 +68,6 @@ from novelwriter.types import ( QtMoveAnchor, QtMoveLeft, QtMoveRight ) -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -107,15 +103,14 @@ class GuiDocEditor(QPlainTextEdit): requestProjectItemSelected = pyqtSignal(str, bool) requestProjectItemRenamed = pyqtSignal(str, str) requestNewNoteCreation = pyqtSignal(str, nwItemClass) + requestNextDocument = pyqtSignal(str, bool) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiDocEditor") # Class Variables - self.mainGui = mainGui - self._nwDocument = None self._nwItem = None @@ -1319,9 +1314,8 @@ class GuiDocEditor(QPlainTextEdit): self.docSearch.setResultCount(0, 0) self._lastFind = None if CONFIG.searchNextFile and not goBack: - self.mainGui.openNextDocument( - self._docHandle, wrapAround=CONFIG.searchLoop - ) + self.requestNextDocument.emit(self._docHandle, CONFIG.searchLoop) + QApplication.processEvents() self.beginSearch() self.setFocus() return @@ -1340,9 +1334,8 @@ class GuiDocEditor(QPlainTextEdit): if resIdx > maxIdx and self._docHandle: if CONFIG.searchNextFile and not goBack: - self.mainGui.openNextDocument( - self._docHandle, wrapAround=CONFIG.searchLoop - ) + self.requestNextDocument.emit(self._docHandle, CONFIG.searchLoop) + QApplication.processEvents() self.beginSearch() self.setFocus() return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 6643df47..68aa8c46 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -29,9 +29,8 @@ import logging from enum import Enum from time import time -from typing import TYPE_CHECKING -from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSlot, pyqtSignal +from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, @@ -52,9 +51,6 @@ from novelwriter.types import ( QtUserRole ) -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -74,10 +70,8 @@ class GuiNovelView(QWidget): selectedItemChanged = pyqtSignal(str) openDocumentRequest = pyqtSignal(str, Enum, str, bool) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) - - self.mainGui = mainGui + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) # Build GUI self.novelTree = GuiNovelTree(self) @@ -202,7 +196,6 @@ class GuiNovelToolBar(QWidget): logger.debug("Create: GuiNovelToolBar") self.novelView = novelView - self.mainGui = novelView.mainGui iSz = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(2) @@ -378,7 +371,6 @@ class GuiNovelTree(QTreeWidget): logger.debug("Create: GuiNovelTree") self.novelView = novelView - self.mainGui = novelView.mainGui # Internal Variables self._treeMap = {} @@ -493,7 +485,7 @@ class GuiNovelTree(QTreeWidget): if rootHandle is None: rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL) - treeChanged = self.mainGui.projView.changedSince(self._lastBuild) + treeChanged = SHARED.mainGui.projView.changedSince(self._lastBuild) indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.debug("No changes have been made to the novel index") diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index ddb03f5e..34a324da 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -30,7 +30,6 @@ import logging from enum import Enum from time import time -from typing import TYPE_CHECKING from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette @@ -57,9 +56,6 @@ from novelwriter.types import ( QtUserRole ) -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -81,10 +77,8 @@ class GuiProjectView(QWidget): # Requests for the main GUI projectSettingsRequest = pyqtSignal(int) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) - - self.mainGui = mainGui + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) # Build GUI self.projTree = GuiProjectTree(self) @@ -263,7 +257,6 @@ class GuiProjectToolBar(QWidget): self.projView = projView self.projTree = projView.projTree - self.mainGui = projView.mainGui iSz = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(2) @@ -499,7 +492,6 @@ class GuiProjectTree(QTreeWidget): logger.debug("Create: GuiProjectTree") self.projView = projView - self.mainGui = projView.mainGui # Internal Variables self._treeMap: dict[str, QTreeWidgetItem] = {} @@ -1010,8 +1002,7 @@ class GuiProjectTree(QTreeWidget): trItemP.takeChild(tIndex) for dHandle in reversed(self.getTreeFromHandle(tHandle)): - if self.mainGui.docEditor.docHandle == dHandle: - self.mainGui.closeDocument() + SHARED.closeDocument(dHandle) SHARED.project.removeItem(dHandle) self._treeMap.pop(dHandle, None) @@ -1410,7 +1401,7 @@ class GuiProjectTree(QTreeWidget): if not newFile: itemList.remove(tHandle) - dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList) + dlgMerge = GuiDocMerge(SHARED.mainGui, tHandle, itemList) dlgMerge.exec() if dlgMerge.result() == QDialog.DialogCode.Accepted: @@ -1451,7 +1442,8 @@ class GuiProjectTree(QTreeWidget): if newFile: self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True) - self.mainGui.openDocument(mHandle, doScroll=True) + self.projView.openDocumentRequest.emit(mHandle, nwDocMode.EDIT, "", False) + self.projView.setSelectedHandle(mHandle, doScroll=True) if mrgData.get("moveToTrash", False): for sHandle in reversed(mrgData.get("finalItems", [])): @@ -1480,7 +1472,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Only valid document items can be split") return False - dlgSplit = GuiDocSplit(self.mainGui, tHandle) + dlgSplit = GuiDocSplit(SHARED.mainGui, tHandle) dlgSplit.exec() if dlgSplit.result() == QDialog.DialogCode.Accepted: diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index d5dce50d..0620ad2d 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -27,8 +27,8 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtGui import QPalette from PyQt5.QtCore import QEvent, QPoint, QSize, pyqtSignal +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget from novelwriter import CONFIG, SHARED @@ -58,7 +58,7 @@ class GuiSideBar(QWidget): iSz = QSize(iPx, iPx) self.setContentsMargins(0, 0, 0, 0) - self.installEventFilter(StatusTipFilter(mainGui)) + self.installEventFilter(StatusTipFilter(self.mainGui)) # Buttons self.tbProject = NIconToolButton(self, iSz) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 1992d609..6b51731a 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -27,26 +27,23 @@ import logging from datetime import datetime from time import time -from typing import TYPE_CHECKING, Literal +from typing import Literal -from PyQt5.QtCore import pyqtSlot, QLocale -from PyQt5.QtWidgets import QApplication, QStatusBar, QLabel +from PyQt5.QtCore import QLocale, pyqtSlot +from PyQt5.QtWidgets import QApplication, QLabel, QStatusBar, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime from novelwriter.constants import nwConst from novelwriter.extensions.statusled import StatusLED -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiMainStatus") @@ -238,6 +235,7 @@ class GuiMainStatus(QStatusBar): before starting novelWriter. """ import tracemalloc + from collections import Counter widgets = QApplication.allWidgets() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 533de19e..71b061b2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -263,6 +263,7 @@ class GuiMain(QMainWindow): self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem) self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote) self.docEditor.docTextChanged.connect(self.projSearch.textChanged) + self.docEditor.requestNextDocument.connect(self.openNextDocument) self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -549,36 +550,30 @@ class GuiMain(QMainWindow): return True - def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool: + @pyqtSlot(str, bool) + def openNextDocument(self, tHandle: str, wrapAround: bool) -> None: """Open the next document in the project tree, following the document with the given handle. Stop when reaching the end. """ - if not SHARED.hasProject: - logger.error("No project open") - return False - - nHandle = None # The next handle after tHandle - fHandle = None # The first file handle we encounter - foundIt = False # We've found tHandle, pick the next we see - for tItem in SHARED.project.tree: - if not tItem.isFileType(): - continue - if fHandle is None: - fHandle = tItem.itemHandle - if tItem.itemHandle == tHandle: - foundIt = True - elif foundIt: - nHandle = tItem.itemHandle - break - - if nHandle is not None: - self.openDocument(nHandle, tLine=1, doScroll=True) - return True - elif wrapAround: - self.openDocument(fHandle, tLine=1, doScroll=True) - return False - - return False + if SHARED.hasProject: + nHandle = None # The next handle after tHandle + fHandle = None # The first file handle we encounter + foundIt = False # We've found tHandle, pick the next we see + for tItem in SHARED.project.tree: + if not tItem.isFileType(): + continue + if fHandle is None: + fHandle = tItem.itemHandle + if tItem.itemHandle == tHandle: + foundIt = True + elif foundIt: + nHandle = tItem.itemHandle + break + if nHandle is not None: + self.openDocument(nHandle, tLine=1, doScroll=True) + elif wrapAround: + self.openDocument(fHandle, tLine=1, doScroll=True) + return @pyqtSlot() def saveDocument(self) -> None: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 40d08c1f..6586f676 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -171,6 +171,12 @@ class SharedData(QObject): logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount()) return + def closeDocument(self, tHandle: str | None = None) -> None: + """Close the document editor, optionally a specific document.""" + if tHandle is None or tHandle == self.mainGui.docEditor.docHandle: + self.mainGui.closeDocument() + return + def saveDocument(self) -> None: """Forward save document call to main GUI.""" self.mainGui.saveDocument() diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index ad0a5508..cc59f9f0 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -69,8 +69,8 @@ class GuiWritingStats(NToolDialog): FMT_JSON = 0 FMT_CSV = 1 - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: GuiMain) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiWritingStats") self.setObjectName("GuiWritingStats") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index dc6ffcfc..202d829b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -21,24 +21,25 @@ along with this program. If not, see . from __future__ import annotations import sys -import pytest from shutil import copyfile -from tools import C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE +import pytest -from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMenu, QInputDialog +from PyQt5.QtGui import QPalette +from PyQt5.QtWidgets import QInputDialog, QMenu from novelwriter import CONFIG, SHARED +from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwItemType, nwView, nwWidget -from novelwriter.gui.outline import GuiOutlineView -from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView +from novelwriter.gui.outline import GuiOutlineView +from novelwriter.gui.projtree import GuiProjectTree from novelwriter.tools.welcome import GuiWelcome -from novelwriter.dialogs.editlabel import GuiEditLabel + +from tests.tools import NWD_IGNORE, XML_IGNORE, C, buildTestProject, cmpFiles KEY_DELAY = 1 @@ -50,7 +51,6 @@ def testGuiMain_ProjectBlocker(nwGUI): assert nwGUI.closeProject() is True assert nwGUI.saveProject() is False assert nwGUI.openDocument(None) is False - assert nwGUI.openNextDocument(None) is False assert nwGUI.viewDocument(None) is False assert nwGUI.importDocument() is False @@ -84,7 +84,7 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath): nwGUI.closeProject() # Check that latest release info updated - CONFIG.lastNotes != "0x0" + assert CONFIG.lastNotes != "0x0" # Check that project open dialog launches nwGUI.postLaunchTasks(None) From 917977ae13f1128c730dee956d360305ed36fedb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 2 May 2024 22:30:58 +0200 Subject: [PATCH 4/4] Force document save when manually calling save document --- novelwriter/gui/mainmenu.py | 10 +++++----- novelwriter/guimain.py | 16 +++++++++++----- tests/test_gui/test_gui_guimain.py | 2 ++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 7d6b05cf..6ac50c19 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -25,16 +25,16 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path +from typing import TYPE_CHECKING -from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtWidgets import QMenuBar, QAction +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QAction, QMenuBar from novelwriter import CONFIG, SHARED from novelwriter.common import openExternalPath -from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode +from novelwriter.constants import nwConst, nwKeyWords, nwLabels, nwUnicode, trConst from novelwriter.enum import nwDocAction, nwDocInsert, nwView, nwWidget from novelwriter.extensions.eventfilters import StatusTipFilter @@ -202,7 +202,7 @@ class GuiMainMenu(QMenuBar): # Document > Save self.aSaveDoc = self.docuMenu.addAction(self.tr("Save Document")) self.aSaveDoc.setShortcut("Ctrl+S") - self.aSaveDoc.triggered.connect(self.mainGui.saveDocument) + self.aSaveDoc.triggered.connect(self.mainGui.forceSaveDocument) # Document > Close self.aCloseDoc = self.docuMenu.addAction(self.tr("Close Document")) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 71b061b2..e35699e8 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -575,12 +575,18 @@ class GuiMain(QMainWindow): self.openDocument(fHandle, tLine=1, doScroll=True) return - @pyqtSlot() - def saveDocument(self) -> None: + def saveDocument(self, force: bool = False) -> None: """Save the current documents.""" - self.docEditor.saveCursorPosition() - if SHARED.hasProject and self.docEditor.docChanged: - self.docEditor.saveText() + if SHARED.hasProject: + self.docEditor.saveCursorPosition() + if force or self.docEditor.docChanged: + self.docEditor.saveText() + return + + @pyqtSlot() + def forceSaveDocument(self) -> None: + """Save document even of it has not changed.""" + self.saveDocument(force=True) return def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 202d829b..2a2205e8 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -522,6 +522,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert docEditor.docChanged nwGUI.saveDocument() assert docEditor.docChanged is False + nwGUI.forceSaveDocument() + assert docEditor.docChanged is False nwGUI.rebuildIndex() # Open and view the edited document