From a17e73b5a2b979db791843b64706fc04ccffe143 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Aug 2023 17:08:53 +0200 Subject: [PATCH] Let the shared class handle alert dialogs --- novelwriter/core/coretools.py | 19 ++-- novelwriter/core/project.py | 5 +- novelwriter/dialogs/projload.py | 9 +- novelwriter/dialogs/projsettings.py | 8 +- novelwriter/dialogs/wordlist.py | 11 +-- novelwriter/gui/doceditor.py | 29 +++--- novelwriter/gui/projtree.py | 50 +++++------ novelwriter/guimain.py | 104 ++++++---------------- novelwriter/shared.py | 12 +-- novelwriter/tools/manusbuild.py | 14 +-- novelwriter/tools/manuscript.py | 4 +- novelwriter/tools/manussettings.py | 4 +- novelwriter/tools/writingstats.py | 9 +- tests/mocked.py | 28 ------ tests/test_core/test_core_coretools.py | 14 +-- tests/test_gui/test_gui_mainmenu.py | 19 +--- tests/test_tools/test_tools_manusbuild.py | 4 +- 17 files changed, 107 insertions(+), 236 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 7e161f02..017ea082 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -28,21 +28,17 @@ from __future__ import annotations import shutil import logging -from typing import TYPE_CHECKING, Iterable +from typing import Iterable from functools import partial from PyQt5.QtCore import QCoreApplication -from novelwriter import CONFIG -from novelwriter.enum import nwAlert +from novelwriter import CONFIG, SHARED from novelwriter.common import minmax, simplified from novelwriter.constants import nwItemClass from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -312,8 +308,7 @@ class ProjectBuilder: parameter provided by the New Project Wizard. """ - def __init__(self, mainGui: GuiMain) -> None: - self.mainGui = mainGui + def __init__(self) -> None: self.tr = partial(QCoreApplication.translate, "NWProject") return @@ -478,17 +473,17 @@ class ProjectBuilder: try: shutil.unpack_archive(pkgSample, projPath) except Exception as exc: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Failed to create a new example project." - ), level=nwAlert.ERROR, exc=exc) + ), exc=exc) return False else: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Failed to create a new example project. " "Could not find the necessary files. " "They seem to be missing from this installation." - ), level=nwAlert.ERROR) + )) return False return True diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 4b427721..b0ed4ad2 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -216,7 +216,7 @@ class NWProject(QObject): # Project Methods ## - def openProject(self, projPath: str | Path) -> bool: + def openProject(self, projPath: str | Path, clearLock: bool = False) -> bool: """Open the project file provided. If it doesn't exist, assume it is a folder and look for the file within it. If successful, parse the XML of the file and populate the project variables and @@ -230,6 +230,9 @@ class NWProject(QObject): # Project Lock # ============ + if clearLock: + self._storage.clearLockFile() + lockStatus = self._storage.readLockFile() if len(lockStatus) > 0: if lockStatus[0] == "ERROR": diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index d2f0f7f6..d15635a5 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -25,6 +25,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from pathlib import Path from datetime import datetime @@ -40,6 +41,9 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import formatInt from novelwriter.constants import nwFiles +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -55,13 +59,12 @@ class GuiProjectLoad(QDialog): D_PATH = Qt.ItemDataRole.UserRole - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiProjectLoad") self.setObjectName("GuiProjectLoad") - self.mainGui = mainGui self.openState = self.NONE_STATE self.openPath = None @@ -225,7 +228,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: projName = selList[0].text(self.C_NAME) - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "Remove '{0}' from the recent projects list? " "The project files will not be deleted." ).format(projName)) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 1680117d..dac33288 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -35,7 +35,6 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwAlert from novelwriter.common import simplified from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.pageddialog import NPagedDialog @@ -288,8 +287,6 @@ class GuiProjectEditStatus(QWidget): def __init__(self, projGui, isStatus): super().__init__(parent=projGui) - self.mainGui = projGui.mainGui - if isStatus: self.theStatus = SHARED.project.data.itemStatus pageLabel = self.tr("Novel File Status Levels") @@ -441,9 +438,7 @@ class GuiProjectEditStatus(QWidget): if isinstance(selItem, QTreeWidgetItem): iRow = self.listBox.indexOfTopLevelItem(selItem) if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: - self.mainGui.makeAlert(self.tr( - "Cannot delete a status item that is in use." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Cannot delete a status item that is in use.")) else: self.listBox.takeTopLevelItem(iRow) self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) @@ -574,7 +569,6 @@ class GuiProjectEditReplace(QWidget): def __init__(self, projGui): super().__init__(parent=projGui) - self.mainGui = projGui.mainGui self.arChanged = False wCol0 = CONFIG.pxInt( diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index e1f3704f..7bfb5d95 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -34,7 +34,6 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwAlert from novelwriter.core.spellcheck import UserDictionary if TYPE_CHECKING: # pragma: no cover @@ -52,8 +51,6 @@ class GuiWordList(QDialog): self.setObjectName("GuiWordList") self.setWindowTitle(self.tr("Project Word List")) - self.mainGui = mainGui - mS = CONFIG.pxInt(250) wW = CONFIG.pxInt(320) wH = CONFIG.pxInt(340) @@ -123,15 +120,13 @@ class GuiWordList(QDialog): """Add a new word to the word list.""" word = self.newEntry.text().strip() if word == "": - self.mainGui.makeAlert(self.tr( - "Cannot add a blank word." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Cannot add a blank word.")) return if self.listBox.findItems(word, Qt.MatchExactly): - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The word '{0}' is already in the word list." - ).format(word), level=nwAlert.ERROR) + ).format(word)) return self.listBox.addItem(word) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 633805d8..cbe7f260 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -50,7 +50,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass +from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.common import minmax, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.core.index import countWords @@ -380,14 +380,14 @@ class GuiDocEditor(QTextEdit): docSize = len(theDoc) if docSize > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The document you are trying to open is too big. " "The document size is {0} MB. " "The maximum size allowed is {1} MB." ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) self.clearEditor() return False @@ -478,14 +478,14 @@ class GuiDocEditor(QTextEdit): """ docSize = len(theText) if docSize > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The text you are trying to add is too big. " "The text size is {0} MB. " "The maximum size allowed is {1} MB." ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) return False qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) @@ -524,7 +524,7 @@ class GuiDocEditor(QTextEdit): if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument._currHash != self._nwDocument._prevHash: - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "This document has been changed outside of novelWriter " "while it was open. Overwrite the file on disk?" )) @@ -532,10 +532,9 @@ class GuiDocEditor(QTextEdit): saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) if not saveOk: - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not save document."), - info=self._nwDocument.getError(), - level=nwAlert.ERROR + info=self._nwDocument.getError() ) return False @@ -723,7 +722,7 @@ class GuiDocEditor(QTextEdit): if not CONFIG.hasEnchant: if theMode: - self.mainGui.makeAlert(self.tr( + SHARED.info(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." )) @@ -867,7 +866,7 @@ class GuiDocEditor(QTextEdit): if self._nwDocument is None: logger.error("No document open") return False - self.mainGui.makeAlert( + SHARED.info( self.tr("The currently open file is saved in:"), info=self._nwDocument.getFileLocation() ) @@ -1103,12 +1102,12 @@ class GuiDocEditor(QTextEdit): self._lastFind = None if self.document().characterCount() > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The document has grown too big and you cannot add more text to it. " "The maximum size of a single novelWriter document is {0} MB." ).format( f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) self.undo() return @@ -1665,9 +1664,7 @@ class GuiDocEditor(QTextEdit): """ theCursor = self.textCursor() if not theCursor.hasSelection(): - self.mainGui.makeAlert(self.tr( - "Please select some text before calling replace quotes." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Please select some text before calling replace quotes.")) return False posS = theCursor.selectionStart() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 52235374..1905bdec 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -49,7 +49,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.enum import ( - nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwWidget + nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget ) if TYPE_CHECKING: # pragma: no cover @@ -582,9 +582,7 @@ class GuiProjectTree(QTreeWidget): sHandle = self.getSelectedHandle() pItem = SHARED.project.tree[sHandle] if sHandle else None if sHandle is None or pItem is None: - self.mainGui.makeAlert(self.tr( - "Did not find anywhere to add the file or folder!" - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Did not find anywhere to add the file or folder!")) return False # Collect some information about the selected item @@ -593,9 +591,7 @@ class GuiProjectTree(QTreeWidget): sIsParent = False if qItem is None else qItem.childCount() > 0 if SHARED.project.tree.isTrash(sHandle): - self.mainGui.makeAlert(self.tr( - "Cannot add new files or folders to the Trash folder." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Cannot add new files or folders to the Trash folder.")) return False # Set default label and determine if new item is to be added @@ -831,9 +827,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Emptying Trash folder") if trashHandle is None: - self.mainGui.makeAlert(self.tr( - "There is currently no Trash folder in this project." - )) + SHARED.info(self.tr("There is currently no Trash folder in this project.")) return False theTrash = self.getTreeFromHandle(trashHandle) @@ -842,12 +836,10 @@ class GuiProjectTree(QTreeWidget): nTrash = len(theTrash) if nTrash == 0: - self.mainGui.makeAlert(self.tr( - "The Trash folder is already empty." - )) + SHARED.info(self.tr("The Trash folder is already empty.")) return False - msgYes = self.mainGui.askQuestion( + msgYes = SHARED.question( self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) ) if not msgYes: @@ -893,8 +885,8 @@ class GuiProjectTree(QTreeWidget): return False if askFirst: - msgYes = self.mainGui.askQuestion( - self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), + msgYes = SHARED.question( + self.tr("Move '{0}' to Trash?").format(nwItemS.itemName) ) if not msgYes: logger.info("Action cancelled by user") @@ -928,9 +920,7 @@ class GuiProjectTree(QTreeWidget): if nwItemS.isRootType(): # Only an empty ROOT folder can be deleted if trItemS.childCount() > 0: - self.mainGui.makeAlert(self.tr( - "Root folders can only be deleted when they are empty." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Root folders can only be deleted when they are empty.")) return False logger.debug("Permanently deleting root folder '%s'", tHandle) @@ -948,7 +938,7 @@ class GuiProjectTree(QTreeWidget): else: if askFirst: - msgYes = self.mainGui.askQuestion( + msgYes = SHARED.question( self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) ) if not msgYes: @@ -1517,7 +1507,7 @@ class GuiProjectTree(QTreeWidget): """Convert a folder to a note or document.""" tItem = SHARED.project.tree[tHandle] if tItem is not None and tItem.isFolderType(): - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "Do you want to convert the folder to a {0}? " "This action cannot be reversed." ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))) @@ -1559,7 +1549,7 @@ class GuiProjectTree(QTreeWidget): mrgData = dlgMerge.getData() mrgList = mrgData.get("finalItems", []) if not mrgList: - self.mainGui.makeAlert(self.tr("No documents selected for merging.")) + SHARED.info(self.tr("No documents selected for merging.")) return False # Save the open document first, in case it's part of merge @@ -1582,9 +1572,9 @@ class GuiProjectTree(QTreeWidget): docMerger.appendText(sHandle, True, mLabel) if not docMerger.writeTargetDoc(): - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not write document content."), - info=docMerger.getError(), level=nwAlert.ERROR + info=docMerger.getError() ) return False @@ -1646,9 +1636,9 @@ class GuiProjectTree(QTreeWidget): self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) self._alertTreeChange(dHandle, flush=False) if not writeOk: - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not write document content."), - info=docSplit.getError(), level=nwAlert.ERROR + info=docSplit.getError() ) if splitData.get("moveToTrash", False): @@ -1673,7 +1663,7 @@ class GuiProjectTree(QTreeWidget): else: question = self.tr("Do you want to duplicate this item and all child items?") - if not self.mainGui.askQuestion(question): + if not SHARED.question(question): return False docDup = DocDuplicator(SHARED.project) @@ -1685,7 +1675,7 @@ class GuiProjectTree(QTreeWidget): dupCount += 1 if dupCount != nItems: - self.mainGui.makeAlert(self.tr("Could not duplicate all items."), level=nwAlert.WARN) + SHARED.warn(self.tr("Could not duplicate all items.")) self.saveTreeOrder() @@ -1742,9 +1732,9 @@ class GuiProjectTree(QTreeWidget): elif pHandle and pHandle in self._treeMap: pItem = self._treeMap[pHandle] else: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "There is nowhere to add item with name '{0}'." - ).format(nwItem.itemName), level=nwAlert.ERROR) + ).format(nwItem.itemName)) return None byIndex = -1 diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index cabfeda9..3b7f7e8a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -31,7 +31,7 @@ from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence, QPixmap +from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence from PyQt5.QtWidgets import ( qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget @@ -63,10 +63,10 @@ from novelwriter.core.project import NWProject from novelwriter.core.coretools import ProjectBuilder from novelwriter.enum import ( - nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView + nwDocAction, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView ) from novelwriter.common import getGuiItem, hexToInt -from novelwriter.constants import nwFiles, nwLabels, trConst +from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) @@ -133,7 +133,6 @@ class GuiMain(QMainWindow): # ============= # Sizes - iPx = SHARED.theme.fontPixelSize mPx = CONFIG.pxInt(4) hWd = CONFIG.pxInt(4) @@ -301,15 +300,6 @@ class GuiMain(QMainWindow): keyEscape.setKey(QKeySequence(Qt.Key_Escape)) keyEscape.activated.connect(self._keyPressEscape) - # Cache Alert Pixmaps - pxSize = (2*iPx, 2*iPx) - self.alertPix: dict[nwAlert, QPixmap] = { - nwAlert.INFO: SHARED.theme.getPixmap("alert_info", pxSize), - nwAlert.WARN: SHARED.theme.getPixmap("alert_warn", pxSize), - nwAlert.ERROR: SHARED.theme.getPixmap("alert_error", pxSize), - nwAlert.ASK: SHARED.theme.getPixmap("alert_question", pxSize), - } - # Check that config loaded fine self.reportConfErr() @@ -325,11 +315,11 @@ class GuiMain(QMainWindow): logger.debug("Ready: GUI") if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: - self.makeAlert(self.tr( + SHARED.warn(self.tr( "You are running an untested development version of novelWriter. " "Please be careful when working on a live project " "and make sure you take regular backups." - ), level=nwAlert.WARN) + )) logger.info("novelWriter is ready ...") self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) @@ -377,9 +367,9 @@ class GuiMain(QMainWindow): """Create a new project via the new project wizard.""" if SHARED.hasProject: if not self.closeProject(): - self.makeAlert(self.tr( + SHARED.error(self.tr( "Cannot create a new project when another project is open." - ), level=nwAlert.ERROR) + )) return False if projData is None: @@ -394,14 +384,14 @@ class GuiMain(QMainWindow): return False if (Path(projPath) / nwFiles.PROJ_FILE).is_file(): - self.makeAlert(self.tr( + SHARED.error(self.tr( "A project already exists in that location. " "Please choose another folder." - ), level=nwAlert.ERROR) + )) return False logger.info("Creating new project") - nwProject = ProjectBuilder(self) + nwProject = ProjectBuilder() if nwProject.buildProject(projData): self.openProject(projPath) else: @@ -419,7 +409,7 @@ class GuiMain(QMainWindow): return True if not isYes: - msgYes = self.askQuestion("%s
%s" % ( + msgYes = SHARED.question("%s
%s" % ( self.tr("Close the current project?"), self.tr("Changes are saved automatically.") )) @@ -434,7 +424,7 @@ class GuiMain(QMainWindow): if SHARED.project.data.doBackup and CONFIG.backupOnClose: doBackup = True if CONFIG.askBeforeBackup: - doBackup = self.askQuestion(self.tr("Backup the current project?")) + doBackup = SHARED.question(self.tr("Backup the current project?")) if doBackup: SHARED.project.backupProject(False) @@ -505,9 +495,8 @@ class GuiMain(QMainWindow): except Exception: lockDetails = "" - if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN): - SHARED.unlockProject() - if not SHARED.openProject(projFile): + if SHARED.question(lockText, info=lockInfo, details=lockDetails, warn=True): + if not SHARED.openProject(projFile, clearLock=True): return False else: return False @@ -545,7 +534,7 @@ class GuiMain(QMainWindow): # Check if we need to rebuild the index if SHARED.project.index.indexBroken: - self.makeAlert(self.tr("The project index is outdated or broken. Rebuilding index.")) + SHARED.info(self.tr("The project index is outdated or broken. Rebuilding index.")) self.rebuildIndex() # Make sure the changed status is set to false on things opened @@ -731,19 +720,19 @@ class GuiMain(QMainWindow): theText = inFile.read() CONFIG.setLastPath(loadFile) except Exception as exc: - self.makeAlert(self.tr( + SHARED.error(self.tr( "Could not read file. The file must be an existing text file." - ), level=nwAlert.ERROR, exc=exc) + ), exc=exc) return False if self.docEditor.docHandle is None: - self.makeAlert(self.tr( + SHARED.error(self.tr( "Please open a document to import the text file into." - ), level=nwAlert.ERROR) + )) return False if not self.docEditor.isEmpty: - msgYes = self.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "Importing the file will overwrite the current content of " "the document. Do you want to proceed?" )) @@ -845,7 +834,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() if not beQuiet: - self.makeAlert(self.tr("The project index has been successfully rebuilt.")) + SHARED.info(self.tr("The project index has been successfully rebuilt.")) return True @@ -891,7 +880,7 @@ class GuiMain(QMainWindow): self.saveDocument() if dlgConf.needsRestart: - self.makeAlert(self.tr( + SHARED.info(self.tr( "Some changes will not be applied until novelWriter has been restarted." )) @@ -1074,54 +1063,13 @@ class GuiMain(QMainWindow): return - def makeAlert(self, text: str, info: str = "", details: str = "", - level: nwAlert = nwAlert.INFO, exc: Exception | None = None) -> None: - """Alert both the user and the logger at the same time.""" - logText = " ".join(filter(None, [text, info, details])) - if level == nwAlert.INFO: - logger.info(logText, stacklevel=2) - elif level == nwAlert.WARN: - logger.warning(logText, stacklevel=2) - elif level == nwAlert.ERROR: - logger.error(logText, stacklevel=2, exc_info=exc) - - if exc is not None: - tExc = f"{type(exc).__name__}: {str(exc)}" - info = f"{info}
{tExc}" if info else tExc - - msgBox = QMessageBox(self) - msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) - msgBox.setText(text) - msgBox.setInformativeText(info) - msgBox.setDetailedText(details) - msgBox.setStandardButtons(QMessageBox.Ok) - msgBox.setIconPixmap(self.alertPix[level]) - msgBox.adjustSize() - msgBox.exec_() - - return - - def askQuestion(self, text: str, info: str = "", details: str = "", - level: nwAlert = nwAlert.ASK) -> bool: - """Ask the user a Yes/No question, and return the answer.""" - msgBox = QMessageBox(self) - msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) - msgBox.setText(text) - msgBox.setInformativeText(info) - msgBox.setDetailedText(details) - msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msgBox.setIconPixmap(self.alertPix[level]) - msgBox.adjustSize() - msgBox.exec_() - return msgBox.result() == QMessageBox.Yes - def reportConfErr(self) -> bool: """Checks if the Config module has any errors to report, and let the user know if this is the case. The Config module caches errors since it is initialised before the GUI itself. """ if CONFIG.hasError: - self.makeAlert(CONFIG.errorText(), level=nwAlert.ERROR) + SHARED.error(CONFIG.errorText()) return True return False @@ -1132,7 +1080,7 @@ class GuiMain(QMainWindow): def closeMain(self) -> bool: """Save everything, and close novelWriter.""" if SHARED.hasProject: - msgYes = self.askQuestion("%s
%s" % ( + msgYes = SHARED.question("%s
%s" % ( self.tr("Do you want to exit novelWriter?"), self.tr("Changes are saved automatically.") )) @@ -1380,13 +1328,13 @@ class GuiMain(QMainWindow): """ tHandle, sTitle = SHARED.project.index.getTagSource(tag) if tHandle is None: - self.makeAlert(self.tr( + 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" - ), level=nwAlert.ERROR) + )) return None, None return tHandle, sTitle diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 4df3d24e..20a4677f 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -104,14 +104,14 @@ class SharedData(QObject): logger.debug("SharedData instance initialised") return - def openProject(self, path: str | Path) -> bool: + def openProject(self, path: str | Path, clearLock: bool = False) -> bool: """Open a project.""" if self.project.isValid: logger.error("A project is already open") return False self._lockedBy = None - status = self.project.openProject(path) + status = self.project.openProject(path, clearLock=clearLock) if status is False: # We must cache the lock status before resetting the project self._lockedBy = self.project.lockStatus @@ -132,10 +132,6 @@ class SharedData(QObject): self._resetProject() return - def unlockProject(self) -> bool: - """Remove the project lock.""" - return self.project.storage.clearLockFile() - ## # Alert Boxes ## @@ -171,10 +167,10 @@ class SharedData(QObject): return def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: - """Open an error alert box.""" + """Open a question box.""" self._alert = _GuiAlert(self.mainGui, self.theme) self._alert.setMessage(text, info, details) - self._alert.setAlertType(_GuiAlert.ERROR, True) + self._alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) self._alert.exec_() return self._alert.result() == QMessageBox.Yes diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 9aafaac0..390d626c 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -25,7 +25,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot @@ -36,7 +35,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwAlert, nwBuildFmt +from novelwriter.enum import nwBuildFmt from novelwriter.common import makeFileNameSafe from novelwriter.constants import nwLabels from novelwriter.core.item import NWItem @@ -44,9 +43,6 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.buildsettings import BuildSettings from novelwriter.extensions.simpleprogress import NProgressSimple -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -59,14 +55,12 @@ class GuiManuscriptBuild(QDialog): D_KEY = Qt.ItemDataRole.UserRole - def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings): + def __init__(self, parent: QWidget, build: BuildSettings): super().__init__(parent=parent) logger.debug("Create: GuiManuscriptBuild") self.setObjectName("GuiManuscriptBuild") - self.mainGui = mainGui - self._parent = parent self._build = build @@ -308,14 +302,14 @@ class GuiManuscriptBuild(QDialog): self.buildProgress.setValue(0) bPath = Path(self.buildPath.text()) if not bPath.is_dir(): - self.mainGui.makeAlert(self.tr("Output folder does not exist."), level=nwAlert.ERROR) + SHARED.error(self.tr("Output folder does not exist.")) return False bExt = nwLabels.BUILD_EXT[bFormat] buildPath = (bPath / makeFileNameSafe(bName)).with_suffix(bExt) if buildPath.exists(): - if not self.mainGui.askQuestion( + if not SHARED.question( self.tr("The file already exists. Do you want to overwrite it?") ): return False diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 13b6f089..1a1e4d1d 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -268,7 +268,7 @@ class GuiManuscript(QDialog): """Delete the currently selected build settings entry.""" build = self._getSelectedBuild() if build is not None: - if self.mainGui.askQuestion(self.tr("Delete build '{0}'?".format(build.name))): + if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))): self._builds.removeBuild(build.buildID) self._updateBuildsList() return @@ -325,7 +325,7 @@ class GuiManuscript(QDialog): """Open the build dialog and build the manuscript.""" build = self._getSelectedBuild() if isinstance(build, BuildSettings): - dlgBuild = GuiManuscriptBuild(self, self.mainGui, build) + dlgBuild = GuiManuscriptBuild(self, build) dlgBuild.exec_() # After the build is done, save build settings changes diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index dc7195f6..842114a9 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -76,8 +76,6 @@ class GuiBuildSettings(QDialog): if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - self.mainGui = mainGui - self._build = build self.setWindowTitle(self.tr("Manuscript Build Settings")) @@ -245,7 +243,7 @@ class GuiBuildSettings(QDialog): whether the user wants to save them. """ if self._build.changed: - response = self.mainGui.askQuestion(self.tr( + response = SHARED.question(self.tr( "Do you want to save your changes to '{0}'?".format(self._build.name) )) if response: diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 83da4cb7..74b11da7 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -37,7 +37,6 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.enum import nwAlert from novelwriter.error import formatException from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax from novelwriter.constants import nwConst @@ -72,8 +71,6 @@ class GuiWritingStats(QDialog): if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - self.mainGui = mainGui - self.logData = [] self.filterData = [] self.timeFilter = 0.0 @@ -413,14 +410,14 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: - self.mainGui.makeAlert( + SHARED.info( self.tr("{0} file successfully written to:").format(textFmt), info=savePath ) else: - self.mainGui.makeAlert( + SHARED.error( self.tr("Failed to write {0} file.").format(textFmt), - info=errMsg, level=nwAlert.ERROR + info=errMsg ) return wSuccess diff --git a/tests/mocked.py b/tests/mocked.py index 0960138f..a22258f5 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -31,31 +31,13 @@ class MockGuiMain(QWidget): def __init__(self): super().__init__() - self.mainStatus = MockStatusBar() self.projPath = "" - - # Test Variables - self.askResponse = True - self.lastAlert = "" - self.lastQuestion = "" - return def postLaunchTasks(self, cmdOpen): return - def makeAlert(self, text, info="", detals="", level=0, exc=None): - assert isinstance(text, str) - print("%s: %s" % (str(level), text)) - self.lastAlert = str(text) - return - - def askQuestion(self, text, info="", details="", level=3): - print("Question: %s" % text) - self.lastQuestion = text - return self.askResponse - def setStatus(self, theMessage): return @@ -72,16 +54,6 @@ class MockGuiMain(QWidget): def close(self): return "close" - # Test Functions - - def undo(self): - self.askResponse = True - return - - def clear(self): - self.lastAlert = "" - return - # END Class MockGuiMain diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 765c2e08..6a39d96b 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -410,7 +410,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx" compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx" - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() # Setting no data should fail assert projBuild.buildProject({}) is False @@ -432,7 +432,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): +def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockRnd): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ @@ -460,7 +460,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): "numScenes": 3, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() assert projBuild.buildProject(projData) is True copyfile(projFile, testFile) @@ -470,7 +470,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): +def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockRnd): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ @@ -498,7 +498,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): "numScenes": 6, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() assert projBuild.buildProject(projData) is True copyfile(projFile, testFile) @@ -508,7 +508,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths, mockGUI): +def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths): """Check that we can create a new project can be created from the provided sample project via a zip file. """ @@ -522,7 +522,7 @@ def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths, mockGUI): "popCustom": False, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() # No path set assert projBuild.buildProject({"popSample": True}) is False diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index f0215f41..626c71aa 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -27,7 +27,7 @@ from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox from tools import C, writeFile, buildTestProject -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.gui.doceditor import GuiDocEditor @@ -653,21 +653,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): # Reveal File Location # ==================== - theMessage = "" - - def recordMsg(*args, **kwargs): - nonlocal theMessage - theMessage = "%s|%s" % (args[0], kwargs["info"]) - return None - - assert not theMessage - monkeypatch.setattr(nwGUI, "makeAlert", recordMsg) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) - - theBits = theMessage.split("|") - assert len(theBits) == 2 - assert theBits[0] == "The currently open file is saved in:" - assert theBits[1] == str(projPath / "content" / "000000000000f.nwd") + path = str(projPath / "content" / "000000000000f.nwd") + logMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert logMsg == f"The currently open file is saved in: {path}" # qtbot.stop() diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 7e78f073..271f9b0f 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -45,7 +45,7 @@ def testManuscriptBuild_Main( build = BuildSettings() build.setLastPath(fncPath) - manus = GuiManuscriptBuild(nwGUI, nwGUI, build) + manus = GuiManuscriptBuild(nwGUI, build) manus.show() # Check initial values @@ -101,7 +101,7 @@ def testManuscriptBuild_Main( # Error Handling # ============== - manus = GuiManuscriptBuild(nwGUI, nwGUI, build) + manus = GuiManuscriptBuild(nwGUI, build) manus.show() # Name, path and format should be remembered