diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 0d1df83ae..bcf784122 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -76,6 +76,7 @@ def main(sysArgs: list | None = None): "config=", "data=", "testmode", + "meminfo" ] helpMsg = ( @@ -92,6 +93,7 @@ def main(sysArgs: list | None = None): " -v, --version Print program version and exit.\n" " --info Print additional runtime information.\n" " --debug Print debug output. Includes --info.\n" + " --meminfo Show memory usage information in the status bar.\n" " --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n" " --config= Alternative config file.\n" " --data= Alternative user data path.\n" @@ -127,6 +129,7 @@ def main(sysArgs: list | None = None): elif inOpt == "--info": logLevel = logging.INFO elif inOpt == "--debug": + CONFIG.isDebug = True logLevel = logging.DEBUG logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" elif inOpt == "--style": @@ -137,6 +140,8 @@ def main(sysArgs: list | None = None): dataPath = inArg elif inOpt == "--testmode": testMode = True + elif inOpt == "--meminfo": + CONFIG.memInfo = True # Setup Logging pkgLogger = logging.getLogger(__package__) diff --git a/novelwriter/config.py b/novelwriter/config.py index 390353714..debe960ba 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -225,7 +225,8 @@ def __init__(self) -> None: # Other System Info self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() - self.isDebug = False + self.isDebug = False # True if running in debug mode + self.memInfo = False # True if displaying mem info in status bar # Packages self.hasEnchant = False # The pyenchant package @@ -485,7 +486,6 @@ def initConfig(self, confPath: str | Path | None = None, self._recentObj.loadCache() self._checkOptionalPackages() - self.isDebug = logger.getEffectiveLevel() == logging.DEBUG logger.debug("Config instance initialised") diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 119b44161..6a90c5f02 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -551,7 +551,7 @@ def getHandleHeaderCount(self, tHandle: str) -> int: return 0 def getTableOfContents( - self, rHandle: str, maxDepth: int, skipExcl: bool = True + self, rHandle: str | None, maxDepth: int, skipExcl: bool = True ) -> list[tuple[str, int, str, int]]: """Generate a table of contents up to a maximum depth.""" tOrder = [] diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index c7640ec0a..13d60d18c 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -28,7 +28,7 @@ from datetime import datetime -from PyQt5.QtGui import QCursor +from PyQt5.QtGui import QCloseEvent, QCursor from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( qApp, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QTabWidget, @@ -44,7 +44,7 @@ class GuiAbout(QDialog): - def __init__(self, parent: QWidget): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) logger.debug("Create: GuiAbout") @@ -101,7 +101,7 @@ def __init__(self, parent: QWidget): # OK Button self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok) - self.buttonBox.accepted.connect(self._doClose) + self.buttonBox.accepted.connect(self.close) self.outerBox.addLayout(self.innerBox) self.outerBox.addWidget(self.buttonBox) @@ -111,13 +111,12 @@ def __init__(self, parent: QWidget): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiAbout") return - def populateGUI(self): - """Populate tabs with text. - """ + def populateGUI(self) -> None: + """Populate tabs with text.""" qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) self._setStyleSheet() self._fillAboutPage() @@ -127,19 +126,27 @@ def populateGUI(self): qApp.restoreOverrideCursor() return - def showReleaseNotes(self): - """Show the release notes. - """ + def showReleaseNotes(self) -> None: + """Show the release notes.""" self.tabBox.setCurrentWidget(self.pageNotes) return + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + event.accept() + self.deleteLater() + return + ## # Internal Functions ## - def _fillAboutPage(self): - """Generate the content for the About page. - """ + def _fillAboutPage(self) -> None: + """Generate the content for the About page.""" aboutMsg = ( "

{title1}

" "

{copy}

" @@ -181,9 +188,8 @@ def _fillAboutPage(self): return - def _fillNotesPage(self): - """Load the content for the Release Notes page. - """ + def _fillNotesPage(self) -> None: + """Load the content for the Release Notes page.""" docPath = CONFIG.assetPath("text") / "release_notes.htm" docText = readTextFile(docPath) if docText: @@ -192,9 +198,8 @@ def _fillNotesPage(self): self.pageNotes.setHtml("Error loading release notes text ...") return - def _fillCreditsPage(self): - """Load the content for the Credits page. - """ + def _fillCreditsPage(self) -> None: + """Load the content for the Credits page.""" docPath = CONFIG.assetPath("text") / "credits_en.htm" docText = readTextFile(docPath) if docText: @@ -203,9 +208,8 @@ def _fillCreditsPage(self): self.pageCredits.setHtml("Error loading credits text ...") return - def _fillLicensePage(self): - """Load the content for the Licence page. - """ + def _fillLicensePage(self) -> None: + """Load the content for the Licence page.""" docPath = CONFIG.assetPath("text") / "gplv3_en.htm" docText = readTextFile(docPath) if docText: @@ -214,9 +218,8 @@ def _fillLicensePage(self): self.pageLicense.setHtml("Error loading licence text ...") return - def _setStyleSheet(self): - """Set stylesheet for all browser tabs - """ + def _setStyleSheet(self) -> None: + """Set stylesheet for all browser tabs.""" styleSheet = ( "h1, h2, h3, h4 {{" " color: rgb({hColR},{hColG},{hColB});" @@ -242,8 +245,4 @@ def _setStyleSheet(self): return - def _doClose(self): - self.close() - return - # END Class GuiAbout diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 9af0a1682..e6a2aedd9 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -26,7 +26,8 @@ import logging -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtWidgets import ( QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget @@ -108,13 +109,12 @@ def __init__(self, parent: QWidget, sHandle: str, itemList: list[str]) -> None: return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocMerge") return - def getData(self): - """Return the user's choices. - """ + def getData(self) -> dict: + """Return the user's choices.""" finalItems = [] for i in range(self.listBox.count()): item = self.listBox.item(i) @@ -127,12 +127,22 @@ def getData(self): return self._data ## - # Slots + # Events ## - def _resetList(self): - """Reset the content of the list box to its original state. - """ + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _resetList(self) -> None: + """Reset the content of the list box to its original state.""" logger.debug("Resetting list box content") sHandle = self._data.get("sHandle", None) itemList = self._data.get("origItems", []) @@ -143,9 +153,8 @@ def _resetList(self): # Internal Functions ## - def _loadContent(self, sHandle, itemList): - """Load content from a given list of items. - """ + def _loadContent(self, sHandle: str, itemList: list[str]) -> None: + """Load content from a given list of items.""" self._data = {} self._data["sHandle"] = sHandle self._data["origItems"] = itemList diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 26b54f59e..172ff7e1c 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -26,10 +26,11 @@ import logging -from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView, - QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout + QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QGridLayout, + QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -45,7 +46,7 @@ class GuiDocSplit(QDialog): LEVEL_ROLE = Qt.ItemDataRole.UserRole + 1 LABEL_ROLE = Qt.ItemDataRole.UserRole + 2 - def __init__(self, parent, sHandle): + def __init__(self, parent: QWidget, sHandle: str) -> None: super().__init__(parent=parent) logger.debug("Create: GuiDocSplit") @@ -138,11 +139,11 @@ def __init__(self, parent, sHandle): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocSplit") return - def getData(self): + def getData(self) -> tuple[dict, list]: """Return the user's choices. Also save the users options for the next time the dialog is used. """ @@ -167,6 +168,7 @@ def getData(self): self._data["docHierarchy"] = docHierarchy self._data["moveToTrash"] = moveToTrash + logger.debug("Saving State: GuiDocSplit") pOptions = SHARED.project.options pOptions.setValue("GuiDocSplit", "spLevel", spLevel) pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder) @@ -175,12 +177,22 @@ def getData(self): return self._data, self._text ## - # Slots + # Events ## - def _reloadList(self): - """Reload the content of the list box. - """ + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _reloadList(self) -> None: + """Reload the content of the list box.""" sHandle = self._data.get("sHandle", None) self._loadContent(sHandle) return @@ -189,9 +201,8 @@ def _reloadList(self): # Internal Functions ## - def _loadContent(self, sHandle): - """Load content from a given source item. - """ + def _loadContent(self, sHandle: str) -> None: + """Load content from a given source item.""" self._data = {} self._data["sHandle"] = sHandle diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 662c00f62..5d84b82a0 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -26,7 +26,8 @@ import logging from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QHBoxLayout + QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, + QWidget ) from novelwriter import CONFIG @@ -36,9 +37,10 @@ class GuiEditLabel(QDialog): - def __init__(self, parent, text=""): + def __init__(self, parent: QWidget, text: str = "") -> None: super().__init__(parent=parent) + logger.debug("Create: GuiEditLabel") self.setObjectName("GuiEditLabel") self.setWindowTitle(self.tr("Item Label")) @@ -70,16 +72,26 @@ def __init__(self, parent, text=""): self.setLayout(self.outerBox) + logger.debug("Ready: GuiEditLabel") + + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiEditLabel") return @property - def itemLabel(self): + def itemLabel(self) -> str: return self.labelValue.text() @classmethod - def getLabel(cls, parent, text): + def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]: + """Pop the dialog and return the result.""" cls = GuiEditLabel(parent, text=text) cls.exec_() - return cls.itemLabel, cls.result() == QDialog.Accepted + label = cls.itemLabel + accepted = cls.result() == QDialog.Accepted + cls.deleteLater() + return label, accepted # END Class GuiEditLabel diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 53940447a..a47c38050 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -25,11 +25,11 @@ import logging -from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt, pyqtSlot +from PyQt5.QtGui import QCloseEvent, QFont +from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, - QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox + QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox, qApp ) from novelwriter import CONFIG, SHARED @@ -43,6 +43,8 @@ class GuiPreferences(NPagedDialog): + newPreferencesReady = pyqtSignal(bool, bool, bool, bool) + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -66,9 +68,10 @@ def __init__(self, parent: QWidget) -> None: self.addTab(self.tabAuto, self.tr("Automation")) self.addTab(self.tabQuote, self.tr("Quotes")) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) + self.rejected.connect(self.close) self.addControls(self.buttonBox) self.resize(*CONFIG.preferencesWinSize) @@ -83,29 +86,21 @@ def __init__(self, parent: QWidget) -> None: return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiPreferences") return ## - # Properties + # Events ## - @property - def updateTheme(self) -> bool: - return self._updateTheme - - @property - def updateSyntax(self) -> bool: - return self._updateSyntax - - @property - def needsRestart(self) -> bool: - return self._needsRestart - - @property - def refreshTree(self) -> bool: - return self._refreshTree + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + logger.debug("Close: GuiPreferences") + self._saveWindowSize() + event.accept() + self.deleteLater() + return ## # Private Slots @@ -113,11 +108,7 @@ def refreshTree(self) -> bool: @pyqtSlot() def _doSave(self) -> None: - """Trigger all the save functions in the tabs, and collect the - status of the saves. - """ - logger.debug("Saving new preferences") - + """Trigger save functions in the tabs and emit ready signal.""" self.tabGeneral.saveValues() self.tabProjects.saveValues() self.tabDocs.saveValues() @@ -126,17 +117,13 @@ def _doSave(self) -> None: self.tabAuto.saveValues() self.tabQuote.saveValues() - self._saveWindowSize() CONFIG.saveConfig() - self.accept() - - return + self.newPreferencesReady.emit( + self._needsRestart, self._refreshTree, self._updateTheme, self._updateSyntax + ) + qApp.processEvents() + self.close() - @pyqtSlot() - def _doClose(self) -> None: - """Close the preferences without saving the changes.""" - self._saveWindowSize() - self.reject() return ## @@ -169,7 +156,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: minWidth = CONFIG.pxInt(200) # Select Locale - self.guiLocale = QComboBox() + self.guiLocale = QComboBox(self) self.guiLocale.setMinimumWidth(minWidth) theLangs = CONFIG.listLanguages(CONFIG.LANG_NW) for lang, langName in theLangs: @@ -187,7 +174,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Select Theme - self.guiTheme = QComboBox() + self.guiTheme = QComboBox(self) self.guiTheme.setMinimumWidth(minWidth) self.theThemes = SHARED.theme.listThemes() for themeDir, themeName in self.theThemes: @@ -203,7 +190,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Editor Theme - self.guiSyntax = QComboBox() + self.guiSyntax = QComboBox(self) self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200)) self.theSyntaxes = SHARED.theme.listSyntax() for syntaxFile, syntaxName in self.theSyntaxes: @@ -219,11 +206,11 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Font Family - self.guiFont = QLineEdit() + self.guiFont = QLineEdit(self) self.guiFont.setReadOnly(True) self.guiFont.setFixedWidth(CONFIG.pxInt(162)) self.guiFont.setText(CONFIG.guiFont) - self.fontButton = QPushButton("...") + self.fontButton = QPushButton("...", self) self.fontButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( @@ -378,7 +365,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: # Backup Path self.backupPath = CONFIG.backupPath() - self.backupGetPath = QPushButton(self.tr("Browse")) + self.backupGetPath = QPushButton(self.tr("Browse"), self) self.backupGetPath.clicked.connect(self._backupFolder) self.backupPathRow = self.mainForm.addRow( self.tr("Backup storage location"), @@ -421,7 +408,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Inactive time for idle - self.userIdleTime = QDoubleSpinBox() + self.userIdleTime = QDoubleSpinBox(self) self.userIdleTime.setMinimum(0.5) self.userIdleTime.setMaximum(600.0) self.userIdleTime.setSingleStep(0.5) @@ -496,11 +483,11 @@ def __init__(self, prefsGui: GuiPreferences) -> None: self.mainForm.addGroupLabel(self.tr("Text Style")) # Font Family - self.textFont = QLineEdit() + self.textFont = QLineEdit(self) self.textFont.setReadOnly(True) self.textFont.setFixedWidth(CONFIG.pxInt(162)) self.textFont.setText(CONFIG.textFont) - self.fontButton = QPushButton("...") + self.fontButton = QPushButton("...", self) self.fontButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( @@ -960,7 +947,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: self.mainForm.addGroupLabel(self.tr("Automatic Padding")) # Pad Before - self.fmtPadBefore = QLineEdit() + self.fmtPadBefore = QLineEdit(self) self.fmtPadBefore.setMaxLength(32) self.fmtPadBefore.setText(CONFIG.fmtPadBefore) self.mainForm.addRow( @@ -970,7 +957,7 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Pad After - self.fmtPadAfter = QLineEdit() + self.fmtPadAfter = QLineEdit(self) self.fmtPadAfter.setMaxLength(32) self.fmtPadAfter.setText(CONFIG.fmtPadAfter) self.mainForm.addRow( @@ -1046,13 +1033,13 @@ def __init__(self, prefsGui: GuiPreferences) -> None: self.quoteSym = {} # Single Quote Style - self.quoteSym["SO"] = QLineEdit() + self.quoteSym["SO"] = QLineEdit(self) self.quoteSym["SO"].setMaxLength(1) self.quoteSym["SO"].setReadOnly(True) self.quoteSym["SO"].setFixedWidth(qWidth) self.quoteSym["SO"].setAlignment(Qt.AlignCenter) self.quoteSym["SO"].setText(CONFIG.fmtSQuoteOpen) - self.btnSingleStyleO = QPushButton("...") + self.btnSingleStyleO = QPushButton("...", self) self.btnSingleStyleO.setMaximumWidth(bWidth) self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO")) self.mainForm.addRow( @@ -1062,13 +1049,13 @@ def __init__(self, prefsGui: GuiPreferences) -> None: button=self.btnSingleStyleO ) - self.quoteSym["SC"] = QLineEdit() + self.quoteSym["SC"] = QLineEdit(self) self.quoteSym["SC"].setMaxLength(1) self.quoteSym["SC"].setReadOnly(True) self.quoteSym["SC"].setFixedWidth(qWidth) self.quoteSym["SC"].setAlignment(Qt.AlignCenter) self.quoteSym["SC"].setText(CONFIG.fmtSQuoteClose) - self.btnSingleStyleC = QPushButton("...") + self.btnSingleStyleC = QPushButton("...", self) self.btnSingleStyleC.setMaximumWidth(bWidth) self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC")) self.mainForm.addRow( @@ -1079,13 +1066,13 @@ def __init__(self, prefsGui: GuiPreferences) -> None: ) # Double Quote Style - self.quoteSym["DO"] = QLineEdit() + self.quoteSym["DO"] = QLineEdit(self) self.quoteSym["DO"].setMaxLength(1) self.quoteSym["DO"].setReadOnly(True) self.quoteSym["DO"].setFixedWidth(qWidth) self.quoteSym["DO"].setAlignment(Qt.AlignCenter) self.quoteSym["DO"].setText(CONFIG.fmtDQuoteOpen) - self.btnDoubleStyleO = QPushButton("...") + self.btnDoubleStyleO = QPushButton("...", self) self.btnDoubleStyleO.setMaximumWidth(bWidth) self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO")) self.mainForm.addRow( @@ -1095,13 +1082,13 @@ def __init__(self, prefsGui: GuiPreferences) -> None: button=self.btnDoubleStyleO ) - self.quoteSym["DC"] = QLineEdit() + self.quoteSym["DC"] = QLineEdit(self) self.quoteSym["DC"].setMaxLength(1) self.quoteSym["DC"].setReadOnly(True) self.quoteSym["DC"].setFixedWidth(qWidth) self.quoteSym["DC"].setAlignment(Qt.AlignCenter) self.quoteSym["DC"].setText(CONFIG.fmtDQuoteClose) - self.btnDoubleStyleC = QPushButton("...") + self.btnDoubleStyleC = QPushButton("...", self) self.btnDoubleStyleC.setMaximumWidth(bWidth) self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC")) self.mainForm.addRow( diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index f5415267f..18afa3f7d 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -26,8 +26,8 @@ import math import logging +from PyQt5.QtGui import QCloseEvent, QFont from PyQt5.QtCore import Qt, QSize, pyqtSlot -from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QAbstractItemView, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget @@ -45,7 +45,7 @@ class GuiProjectDetails(NPagedDialog): - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) logger.debug("Create: GuiProjectDetails") @@ -71,43 +71,41 @@ def __init__(self, parent): self.addTab(self.tabContents, self.tr("Contents")) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) - self.buttonBox.button(QDialogButtonBox.Close) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) + self.rejected.connect(self.close) self.addControls(self.buttonBox) logger.debug("Ready: GuiProjectDetails") return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiProjectDetails") return - def updateValues(self): - """Set all the values of the pages. - """ + def updateValues(self) -> None: + """Set all the values of the pages.""" self.tabMain.updateValues() self.tabContents.updateValues() return ## - # Slots + # Events ## - def _doClose(self): - """Save settings and close the dialog. - """ + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" self._saveGuiSettings() - self.close() + event.accept() + self.deleteLater() return ## # Internal Functions ## - def _saveGuiSettings(self): - """Save GUI settings. - """ + def _saveGuiSettings(self) -> None: + """Save GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) @@ -122,6 +120,7 @@ def _saveGuiSettings(self): countFrom = self.tabContents.poValue.value() clearDouble = self.tabContents.dblValue.isChecked() + logger.debug("Saving State: GuiProjectDetails") pOptions = SHARED.project.options pOptions.setValue("GuiProjectDetails", "winWidth", winWidth) pOptions.setValue("GuiProjectDetails", "winHeight", winHeight) @@ -141,7 +140,7 @@ def _saveGuiSettings(self): class GuiProjectDetailsMain(QWidget): - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) fPx = SHARED.theme.fontPixelSize @@ -270,7 +269,7 @@ class GuiProjectDetailsContents(QWidget): C_PAGE = 3 C_PROG = 4 - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) # Internal @@ -406,9 +405,8 @@ def __init__(self, parent): return - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ + def getColumnSizes(self) -> list[int]: + """Return the column widths for the tree columns.""" retVals = [ self.tocTree.columnWidth(0), self.tocTree.columnWidth(1), @@ -418,24 +416,21 @@ def getColumnSizes(self): ] return retVals - def updateValues(self): - """Populate the tree. - """ + def updateValues(self) -> None: + """Populate the tree.""" self._currentRoot = None self.novelValue.updateList() self.novelValue.setHandle(self.novelValue.firstHandle) self._prepareData(self.novelValue.firstHandle) self._populateTree() - return ## # Internal Functions ## - def _prepareData(self, rootHandle): - """Extract the information from the project index. - """ + def _prepareData(self, rootHandle: str | None) -> None: + """Extract the information from the project index.""" logger.debug("Populating ToC from handle '%s'", rootHandle) self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2) self._theToC.append(("", 0, self.tr("END"), 0)) @@ -446,9 +441,8 @@ def _prepareData(self, rootHandle): ## @pyqtSlot(str) - def _novelValueChanged(self, tHandle): - """Refresh the tree with another root item. - """ + def _novelValueChanged(self, tHandle: str) -> None: + """Refresh the tree with another root item.""" if tHandle != self._currentRoot: self._prepareData(tHandle) self._populateTree() @@ -456,9 +450,8 @@ def _novelValueChanged(self, tHandle): return @pyqtSlot() - def _populateTree(self): - """Set the content of the chapter/page tree. - """ + def _populateTree(self) -> None: + """Set the content of the chapter/page tree.""" dblPages = self.dblValue.isChecked() wpPage = self.wpValue.value() fstPage = self.poValue.value() - 1 diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index 8259ac0f3..1ab8aa6cd 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -153,7 +153,7 @@ def __init__(self, mainGui: GuiMain) -> None: return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiProjectLoad") return diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 20b11c53c..f8d17af1d 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -27,11 +27,11 @@ from typing import TYPE_CHECKING -from PyQt5.QtGui import QIcon, QPixmap, QColor -from PyQt5.QtCore import Qt, pyqtSlot +from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor +from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG, SHARED @@ -53,6 +53,8 @@ class GuiProjectSettings(NPagedDialog): TAB_IMPORT = 2 TAB_REPLACE = 3 + newProjectSettingsReady = pyqtSignal() + def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: super().__init__(parent=mainGui) @@ -86,7 +88,8 @@ def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) + self.rejected.connect(self.close) self.addControls(self.buttonBox) # Focus Tab @@ -100,6 +103,13 @@ def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiProjectSettings") return + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + self._saveGuiSettings() + event.accept() + self.deleteLater() + return + ## # Private Slots ## @@ -137,18 +147,12 @@ def _doSave(self) -> None: newList = self.tabReplace.getNewList() project.data.setAutoReplace(newList) - self._saveGuiSettings() - self.accept() + self.newProjectSettingsReady.emit() + qApp.processEvents() + self.close() return - @pyqtSlot() - def _doClose(self) -> None: - """Save settings and close the dialog.""" - self._saveGuiSettings() - self.reject() - return - ## # Internal Functions ## @@ -173,6 +177,7 @@ def _saveGuiSettings(self) -> None: statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0)) + logger.debug("Saving State: GuiProjectSettings") pOptions = SHARED.project.options pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 6282c6b64..c8cd8b398 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -25,11 +25,11 @@ import logging -from PyQt5.QtGui import QFontMetrics -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QCloseEvent, QFontMetrics +from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtWidgets import ( - QLabel, QVBoxLayout, QHBoxLayout, QDialog, QDialogButtonBox, - QListWidget, QListWidgetItem, QFrame + QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, + QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG @@ -44,9 +44,12 @@ class GuiQuoteSelect(QDialog): D_KEY = Qt.ItemDataRole.UserRole - def __init__(self, parent=None, currentQuote='"'): + def __init__(self, parent: QWidget, currentQuote: str = '"') -> None: super().__init__(parent=parent) + logger.debug("Create: GuiQuoteSelect") + self.setObjectName("GuiQuoteSelect") + self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() self.labelBox = QVBoxLayout() @@ -87,8 +90,8 @@ def __init__(self, parent=None, currentQuote='"'): # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.buttonBox.accepted.connect(self._doAccept) - self.buttonBox.rejected.connect(self._doReject) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) # Assemble self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop) @@ -102,15 +105,31 @@ def __init__(self, parent=None, currentQuote='"'): self.setLayout(self.outerBox) + logger.debug("Ready: GuiQuoteSelect") + + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiQuoteSelect") return ## - # Slots + # Events ## - def _selectedSymbol(self): - """Update the preview label and the selected quote style. - """ + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _selectedSymbol(self) -> None: + """Update the preview label and the selected quote style.""" selItems = self.listBox.selectedItems() if selItems: theSymbol = selItems[0].data(self.D_KEY) @@ -118,16 +137,4 @@ def _selectedSymbol(self): self.selectedQuote = theSymbol return - def _doAccept(self): - """Ok button clicked. - """ - self.accept() - return - - def _doReject(self): - """Cancel button clicked. - """ - self.reject() - return - # END Class GuiQuoteSelect diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index bbdaf0384..e2752dadc 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -30,7 +30,7 @@ from urllib.request import Request, urlopen from PyQt5.QtGui import QCloseEvent, QCursor -from PyQt5.QtCore import Qt, pyqtSlot +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QWidget, qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel ) @@ -95,7 +95,7 @@ def __init__(self, parent: QWidget) -> None: # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) # Assemble self.innerBox = QHBoxLayout() @@ -169,14 +169,4 @@ def closeEvent(self, event: QCloseEvent) -> None: self.deleteLater() return - ## - # Private Slots - ## - - @pyqtSlot() - def _doClose(self) -> None: - """Close the dialog.""" - self.close() - return - # END Class GuiUpdates diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 7bfb5d95e..0611b8849 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -27,10 +27,11 @@ from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractItemView, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, - QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout + QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, qApp ) from novelwriter import CONFIG, SHARED @@ -44,7 +45,9 @@ class GuiWordList(QDialog): - def __init__(self, mainGui: GuiMain): + newWordListReady = pyqtSignal() + + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiWordList") @@ -87,7 +90,7 @@ def __init__(self, mainGui: GuiMain): self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close) self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) # Assemble # ======== @@ -108,15 +111,27 @@ def __init__(self, mainGui: GuiMain): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiWordList") return ## - # Slots + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the close event and perform cleanup.""" + self._saveGuiSettings() + event.accept() + self.deleteLater() + return + + ## + # Private Slots ## - def _doAdd(self): + @pyqtSlot() + def _doAdd(self) -> None: """Add a new word to the word list.""" word = self.newEntry.text().strip() if word == "": @@ -134,16 +149,17 @@ def _doAdd(self): return - def _doDelete(self): + @pyqtSlot() + def _doDelete(self) -> None: """Delete the selected item.""" selItem = self.listBox.selectedItems() if selItem: self.listBox.takeItem(self.listBox.row(selItem[0])) return - def _doSave(self): + @pyqtSlot() + def _doSave(self) -> None: """Save the new word list and close.""" - self._saveGuiSettings() userDict = UserDictionary(SHARED.project) for i in range(self.listBox.count()): item = self.listBox.item(i) @@ -152,20 +168,16 @@ def _doSave(self): if word: userDict.add(word) userDict.save() - self.accept() - return True - - def _doClose(self): - """Close without saving the word list.""" - self._saveGuiSettings() - self.reject() + self.newWordListReady.emit() + qApp.processEvents() + self.close() return ## # Internal Functions ## - def _loadWordList(self): + def _loadWordList(self) -> None: """Load the project's word list, if it exists.""" userDict = UserDictionary(SHARED.project) userDict.load() @@ -175,11 +187,12 @@ def _loadWordList(self): self.listBox.addItem(word) return - def _saveGuiSettings(self): + def _saveGuiSettings(self) -> None: """Save GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) + logger.debug("Saving State: GuiWordList") pOptions = SHARED.project.options pOptions.setValue("GuiWordList", "winWidth", winWidth) pOptions.setValue("GuiWordList", "winHeight", winHeight) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 5bb41f71e..8b84c39e1 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -140,6 +140,7 @@ class nwDocInsert(Enum): NEW_PAGE = 7 VSPACE_S = 8 VSPACE_M = 9 + LIPSUM = 10 # END Enum nwDocInsert diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b39482287..4d4ac3930 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -58,6 +58,7 @@ from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst from novelwriter.core.item import NWItem from novelwriter.core.index import countWords +from novelwriter.tools.lipsum import GuiLipsum from novelwriter.core.document import NWDocument from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.editordocument import GuiTextDocument @@ -850,18 +851,23 @@ def insertText(self, insert: str | nwDocInsert) -> bool: text = "[vspace:2]" newBlock = True goAfter = False + elif insert == nwDocInsert.LIPSUM: + text = GuiLipsum.getLipsum(self) + newBlock = True + goAfter = False else: return False else: return False - if newBlock: - self.insertNewBlock(text, defaultAfter=goAfter) - else: - cursor = self.textCursor() - cursor.beginEditBlock() - cursor.insertText(text) - cursor.endEditBlock() + if text: + if newBlock: + self.insertNewBlock(text, defaultAfter=goAfter) + else: + cursor = self.textCursor() + cursor.beginEditBlock() + cursor.insertText(text) + cursor.endEditBlock() return True @@ -1144,6 +1150,7 @@ def _openContextMenu(self, pos: QPoint) -> None: # Execute the context menu ctxMenu.exec_(self.viewport().mapToGlobal(pos)) + ctxMenu.deleteLater() return diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 5df058108..d548f0d41 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -378,33 +378,34 @@ def _openContextMenu(self, point: QPoint) -> None: userCursor = self.textCursor() userSelection = userCursor.hasSelection() - mnuContext = QMenu(self) + ctxMenu = QMenu(self) if userSelection: - mnuCopy = QAction(self.tr("Copy"), mnuContext) + mnuCopy = QAction(self.tr("Copy"), ctxMenu) mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) - mnuContext.addAction(mnuCopy) + ctxMenu.addAction(mnuCopy) - mnuContext.addSeparator() + ctxMenu.addSeparator() - mnuSelAll = QAction(self.tr("Select All"), mnuContext) + mnuSelAll = QAction(self.tr("Select All"), ctxMenu) mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) - mnuContext.addAction(mnuSelAll) + ctxMenu.addAction(mnuSelAll) - mnuSelWord = QAction(self.tr("Select Word"), mnuContext) + mnuSelWord = QAction(self.tr("Select Word"), ctxMenu) mnuSelWord.triggered.connect( lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, point) ) - mnuContext.addAction(mnuSelWord) + ctxMenu.addAction(mnuSelWord) - mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext) + mnuSelPara = QAction(self.tr("Select Paragraph"), ctxMenu) mnuSelPara.triggered.connect( lambda: self._makePosSelection(QTextCursor.SelectionType.BlockUnderCursor, point) ) - mnuContext.addAction(mnuSelPara) + ctxMenu.addAction(mnuSelPara) # Open the context menu - mnuContext.exec_(self.viewport().mapToGlobal(point)) + ctxMenu.exec_(self.viewport().mapToGlobal(point)) + ctxMenu.deleteLater() return diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 5444bad36..e601565ac 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -123,6 +123,7 @@ def closeProjectTasks(self) -> None: widths = {} for key, tab in self.kwTabs.items(): widths[key] = tab.getColumnWidths() + logger.debug("Saving State: GuiDocViewerPanel") SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", widths) return diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index fa57fe478..fbb47cd17 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -154,12 +154,12 @@ def _buildProjectMenu(self) -> None: # Project > Project Settings self.aProjectSettings = self.projMenu.addAction(self.tr("Project Settings")) self.aProjectSettings.setShortcut("Ctrl+Shift+,") - self.aProjectSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog()) + self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog) # Project > Project Details self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details")) self.aProjectDetails.setShortcut("Shift+F6") - self.aProjectDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) + self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog) # Project > Separator self.projMenu.addSeparator() @@ -593,8 +593,10 @@ def _buildInsertMenu(self) -> None: ) # Insert > Placeholder Text - self.aLipsumText = self.mInsBreaks.addAction(self.tr("Placeholder Text")) - self.aLipsumText.triggered.connect(lambda: self.mainGui.showLoremIpsumDialog()) + self.aLipsumText = self.insMenu.addAction(self.tr("Placeholder Text")) + self.aLipsumText.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.LIPSUM) + ) return @@ -872,7 +874,7 @@ def _buildToolsMenu(self) -> None: # Tools > Project Word List self.aEditWordList = self.toolsMenu.addAction(self.tr("Project Word List")) - self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog()) + self.aEditWordList.triggered.connect(self.mainGui.showProjectWordListDialog) # Tools > Add Dictionaries if CONFIG.osWindows or CONFIG.isDebug: @@ -902,13 +904,13 @@ def _buildToolsMenu(self) -> None: # Tools > Writing Statistics self.aWritingStats = self.toolsMenu.addAction(self.tr("Writing Statistics")) self.aWritingStats.setShortcut("F6") - self.aWritingStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) + self.aWritingStats.triggered.connect(self.mainGui.showWritingStatsDialog) # Tools > Preferences self.aPreferences = self.toolsMenu.addAction(self.tr("Preferences")) self.aPreferences.setShortcut("Ctrl+,") self.aPreferences.setMenuRole(QAction.PreferencesRole) - self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog()) + self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog) return @@ -920,12 +922,12 @@ def _buildHelpMenu(self) -> None: # Help > About self.aAboutNW = self.helpMenu.addAction(self.tr("About novelWriter")) self.aAboutNW.setMenuRole(QAction.AboutRole) - self.aAboutNW.triggered.connect(lambda: self.mainGui.showAboutNWDialog()) + self.aAboutNW.triggered.connect(self.mainGui.showAboutNWDialog) # Help > About Qt5 self.aAboutQt = self.helpMenu.addAction(self.tr("About Qt5")) self.aAboutQt.setMenuRole(QAction.AboutQtRole) - self.aAboutQt.triggered.connect(lambda: self.mainGui.showAboutQtDialog()) + self.aAboutQt.triggered.connect(self.mainGui.showAboutQtDialog) # Help > Separator self.helpMenu.addSeparator() @@ -961,7 +963,7 @@ def _buildHelpMenu(self) -> None: # Document > Check for Updates self.aUpdates = self.helpMenu.addAction(self.tr("Check for New Release")) - self.aUpdates.triggered.connect(lambda: self.mainGui.showUpdatesDialog()) + self.aUpdates.triggered.connect(self.mainGui.showUpdatesDialog) return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 40aae9efc..d59a668a3 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -145,6 +145,7 @@ def closeProjectTasks(self) -> None: """Run closing project tasks.""" lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize + logger.debug("Saving State: GuiNovelView") pOptions = SHARED.project.options pOptions.setValue("GuiNovelView", "lastCol", lastColType) pOptions.setValue("GuiNovelView", "lastColSize", lastColSize) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 95b2b612d..0a1a3df8a 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -606,6 +606,7 @@ def _saveHeaderState(self) -> None: logHidden, orgWidth if logHidden and logWidth == 0 else logWidth ] + logger.debug("Saving State: GuiOutline") pOptions = SHARED.project.options pOptions.setValue("GuiOutline", "columnState", colState) pOptions.saveSettings() diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 76a944540..1a4387ab4 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -27,6 +27,7 @@ from time import time from typing import TYPE_CHECKING, Literal +from datetime import datetime from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtGui import QColor @@ -50,8 +51,9 @@ def __init__(self, mainGui: GuiMain) -> None: logger.debug("Create: GuiMainStatus") - self._refTime = -1.0 + self._refTime = -1.0 self._userIdle = False + self._debugInfo = False colNone = QColor(*SHARED.theme.statNone) colSaved = QColor(*SHARED.theme.statSaved) @@ -223,4 +225,41 @@ def updateDocumentStatus(self, status: bool) -> None: self.setDocumentStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD) return + ## + # Debug + ## + + def memInfo(self) -> None: # pragma: no cover + """Display memory info on the status bar. This is used to + investigate memory usage and Qt widgets that get left in memory. + Enabled by the --meminfo command line flag. + """ + import tracemalloc + from collections import Counter + + widgets = qApp.allWidgets() + if not self._debugInfo: + if tracemalloc.is_tracing(): + self._traceMallocRef = "Total" + else: + self._traceMallocRef = "Relative" + tracemalloc.start() + self._debugInfo = True + self._wCounts = Counter([type(x).__name__ for x in widgets]) + + if hasattr(self, "_wCounts"): + diff = Counter([type(x).__name__ for x in widgets]) - self._wCounts + for name, count in diff.items(): + logger.debug("Widget '%s': +%d", name, count) + + mem = tracemalloc.get_traced_memory() + stamp = datetime.now().strftime("%H:%M:%S") + self.showMessage(( + f"Debug [{stamp}]" + f" \u2013 Widgets: {len(qApp.allWidgets())}" + f" \u2013 {self._traceMallocRef} Memory: {mem[0]:n}" + f" \u2013 Peak: {mem[1]:n}" + ), 6000) + return + # END Class GuiMainStatus diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index bed57b5cc..04ebab1a2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -56,7 +56,6 @@ from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projsettings import GuiProjectSettings -from novelwriter.tools.lipsum import GuiLipsum from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.projwizard import GuiProjectWizard from novelwriter.tools.dictionaries import GuiDictionaries @@ -66,7 +65,7 @@ from novelwriter.enum import ( nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView ) -from novelwriter.common import getGuiItem, hexToInt +from novelwriter.common import hexToInt from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) @@ -866,213 +865,111 @@ def showNewProjectDialog(self) -> dict | None: return None + @pyqtSlot() def showPreferencesDialog(self) -> None: """Open the preferences dialog.""" - dlgConf = GuiPreferences(self) - dlgConf.exec_() - - if dlgConf.result() == QDialog.Accepted: - logger.debug("Applying new preferences") - self.initMain() - self.saveDocument() - - if dlgConf.needsRestart: - SHARED.info(self.tr( - "Some changes will not be applied until novelWriter has been restarted." - )) - - if dlgConf.refreshTree: - self.projView.populateTree() - - if dlgConf.updateTheme: - # We are doing this manually instead of connecting to - # qApp.paletteChanged since the processing order matters - SHARED.theme.loadTheme() - self.docEditor.updateTheme() - self.docViewer.updateTheme() - self.docViewerPanel.updateTheme() - self.sideBar.updateTheme() - self.projView.updateTheme() - self.novelView.updateTheme() - self.outlineView.updateTheme() - self.itemDetails.updateTheme() - self.mainStatus.updateTheme() - - if dlgConf.updateSyntax: - SHARED.theme.loadSyntax() - self.docEditor.updateSyntaxColours() - - self.docEditor.initEditor() - self.docViewer.initViewer() - self.projView.initSettings() - self.novelView.initSettings() - self.outlineView.initSettings() - - self._updateStatusWordCount() - + dialog = GuiPreferences(self) + dialog.newPreferencesReady.connect(self._processConfigChanges) + dialog.exec_() return + @pyqtSlot() @pyqtSlot(int) - def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> bool: + def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None: """Open the project settings dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgProj = GuiProjectSettings(self, focusTab=focusTab) - dlgProj.exec_() - - if dlgProj.result() == QDialog.Accepted: - logger.debug("Applying new project settings") - SHARED.updateSpellCheckLanguage() - self.itemDetails.refreshDetails() - self._updateWindowTitle(SHARED.project.data.name) - - return True + if SHARED.hasProject: + dialog = GuiProjectSettings(self, focusTab=focusTab) + dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) + dialog.exec_() + return - def showProjectDetailsDialog(self) -> bool: + @pyqtSlot() + def showProjectDetailsDialog(self) -> None: """Open the project details dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgDetails = getGuiItem("GuiProjectDetails") - if dlgDetails is None: - dlgDetails = GuiProjectDetails(self) - assert isinstance(dlgDetails, GuiProjectDetails) - - dlgDetails.setModal(True) - dlgDetails.show() - dlgDetails.raise_() - dlgDetails.updateValues() - - return True + if SHARED.hasProject: + dialog = GuiProjectDetails(self) + dialog.setModal(True) + dialog.show() + dialog.raise_() + qApp.processEvents() + dialog.updateValues() + return @pyqtSlot() - def showBuildManuscriptDialog(self) -> bool: + def showBuildManuscriptDialog(self) -> None: """Open the build manuscript dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgBuild = getGuiItem("GuiManuscript") - if dlgBuild is None: - dlgBuild = GuiManuscript(self) - assert isinstance(dlgBuild, GuiManuscript) - - dlgBuild.setModal(False) - dlgBuild.show() - dlgBuild.raise_() - qApp.processEvents() - - dlgBuild.loadContent() - - return True - - def showLoremIpsumDialog(self) -> bool: - """Open the insert lorem ipsum text dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgLipsum = getGuiItem("GuiLipsum") - if dlgLipsum is None: - dlgLipsum = GuiLipsum(self) - assert isinstance(dlgLipsum, GuiLipsum) - - dlgLipsum.setModal(False) - dlgLipsum.show() - dlgLipsum.raise_() - qApp.processEvents() - - return True + if SHARED.hasProject: + dialog = GuiManuscript(self) + dialog.setModal(False) + dialog.show() + dialog.raise_() + qApp.processEvents() + dialog.loadContent() + return - def showProjectWordListDialog(self) -> bool: + @pyqtSlot() + def showProjectWordListDialog(self) -> None: """Open the project word list dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgWords = GuiWordList(self) - dlgWords.exec_() - - if dlgWords.result() == QDialog.Accepted: - logger.debug("Reloading word list") - SHARED.updateSpellCheckLanguage(reload=True) - self.docEditor.spellCheckDocument() - - return True + if SHARED.hasProject: + dialog = GuiWordList(self) + dialog.newWordListReady.connect(self._processWordListChanges) + dialog.exec_() + return - def showWritingStatsDialog(self) -> bool: + @pyqtSlot() + def showWritingStatsDialog(self) -> None: """Open the session stats dialog.""" - if not SHARED.hasProject: - logger.error("No project open") - return False - - dlgStats = getGuiItem("GuiWritingStats") - if dlgStats is None: - dlgStats = GuiWritingStats(self) - assert isinstance(dlgStats, GuiWritingStats) - - dlgStats.setModal(False) - dlgStats.show() - dlgStats.raise_() - qApp.processEvents() - dlgStats.populateGUI() - - return True - - def showAboutNWDialog(self, showNotes: bool = False) -> bool: - """Show the about dialog for novelWriter.""" - dlgAbout = getGuiItem("GuiAbout") - if dlgAbout is None: - dlgAbout = GuiAbout(self) - assert isinstance(dlgAbout, GuiAbout) + if SHARED.hasProject: + dialog = GuiWritingStats(self) + dialog.setModal(False) + dialog.show() + dialog.raise_() + qApp.processEvents() + dialog.populateGUI() + return - dlgAbout.setModal(True) - dlgAbout.show() - dlgAbout.raise_() + @pyqtSlot() + def showAboutNWDialog(self, showNotes: bool = False) -> None: + """Show the novelWriter about dialog.""" + dialog = GuiAbout(self) + dialog.setModal(True) + dialog.show() + dialog.raise_() qApp.processEvents() - dlgAbout.populateGUI() - + dialog.populateGUI() if showNotes: - dlgAbout.showReleaseNotes() - - return True + dialog.showReleaseNotes() + return + @pyqtSlot() def showAboutQtDialog(self) -> None: - """Show the about dialog for Qt.""" + """Show the Qt about dialog.""" msgBox = QMessageBox(self) msgBox.aboutQt(self, "About Qt") return + @pyqtSlot() def showUpdatesDialog(self) -> None: """Show the check for updates dialog.""" - dlgUpdate = getGuiItem("GuiUpdates") - if dlgUpdate is None: - dlgUpdate = GuiUpdates(self) - assert isinstance(dlgUpdate, GuiUpdates) - - dlgUpdate.setModal(True) - dlgUpdate.show() - dlgUpdate.raise_() + dialog = GuiUpdates(self) + dialog.setModal(True) + dialog.show() + dialog.raise_() qApp.processEvents() - dlgUpdate.checkLatest() - + dialog.checkLatest() return @pyqtSlot() def showDictionariesDialog(self) -> None: """Show the download dictionaries dialog.""" - dlgDicts = GuiDictionaries(self) - dlgDicts.setModal(True) - dlgDicts.show() - dlgDicts.raise_() + dialog = GuiDictionaries(self) + dialog.setModal(True) + dialog.show() + dialog.raise_() qApp.processEvents() - if not dlgDicts.initDialog(): - dlgDicts.close() + if not dialog.initDialog(): + dialog.close() SHARED.error(self.tr("Could not initialise the dialog.")) - return def reportConfErr(self) -> bool: @@ -1238,6 +1135,65 @@ def switchFocus(self, paneNo: nwWidget) -> None: # Private Slots ## + @pyqtSlot(bool, bool, bool, bool) + def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None: + """Refresh GUI based on flags from the Preferences dialog.""" + logger.debug("Applying new preferences") + self.initMain() + self.saveDocument() + + if restart: + SHARED.info(self.tr( + "Some changes will not be applied until novelWriter has been restarted." + )) + + if tree: + self.projView.populateTree() + + if theme: + # We are doing this manually instead of connecting to + # qApp.paletteChanged since the processing order matters + SHARED.theme.loadTheme() + self.docEditor.updateTheme() + self.docViewer.updateTheme() + self.docViewerPanel.updateTheme() + self.sideBar.updateTheme() + self.projView.updateTheme() + self.novelView.updateTheme() + self.outlineView.updateTheme() + self.itemDetails.updateTheme() + self.mainStatus.updateTheme() + + if syntax: + SHARED.theme.loadSyntax() + self.docEditor.updateSyntaxColours() + + self.docEditor.initEditor() + self.docViewer.initViewer() + self.projView.initSettings() + self.novelView.initSettings() + self.outlineView.initSettings() + self._updateStatusWordCount() + + return + + @pyqtSlot() + def _processProjectSettingsChanges(self) -> None: + """Refresh data dependent on project settings.""" + logger.debug("Applying new project settings") + SHARED.updateSpellCheckLanguage() + self.itemDetails.refreshDetails() + self._updateWindowTitle(SHARED.project.data.name) + return + + @pyqtSlot() + def _processWordListChanges(self) -> None: + """Reload project word list.""" + logger.debug("Reloading word list") + SHARED.updateSpellCheckLanguage(reload=True) + self.docEditor.spellCheckDocument() + return + @pyqtSlot(str, nwDocMode) def _followTag(self, tag: str, mode: nwDocMode) -> None: """Follow a tag after user interaction with a link.""" @@ -1321,6 +1277,8 @@ def _timeTick(self) -> None: self.mainStatus.setUserIdle(editIdle or userIdle) SHARED.updateIdleTime(currTime, editIdle or userIdle) self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime) + if CONFIG.memInfo and int(currTime) % 5 == 0: # pragma: no cover + self.mainStatus.memInfo() return @pyqtSlot() diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index d5febc2bf..539133845 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -26,10 +26,10 @@ import random import logging -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QGridLayout, QHBoxLayout, QVBoxLayout, QLabel, QDialogButtonBox, - QSpinBox + QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QSpinBox, + QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -41,15 +41,15 @@ class GuiLipsum(QDialog): - def __init__(self, mainGui): - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiLipsum") self.setObjectName("GuiLipsum") if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - self.mainGui = mainGui + self._lipsumText = "" self.setWindowTitle(self.tr("Insert Placeholder Text")) @@ -92,14 +92,16 @@ def __init__(self, mainGui): # Buttons self.buttonBox = QDialogButtonBox() - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.rejected.connect(self.close) self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close) self.btnClose.setAutoDefault(False) - self.btnSave = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole) - self.btnSave.clicked.connect(self._doInsert) - self.btnSave.setAutoDefault(False) + self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole) + self.btnInsert.clicked.connect(self._doInsert) + self.btnInsert.setAutoDefault(False) + + self.rejected.connect(self.close) # Assemble self.outerBox = QVBoxLayout() @@ -112,33 +114,37 @@ def __init__(self, mainGui): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiLipsum") return + @property + def lipsumText(self) -> str: + """Return the generated text.""" + return self._lipsumText + + @classmethod + def getLipsum(cls, parent: QWidget) -> str: + """Pop the dialog and return the lipsum text.""" + cls = GuiLipsum(parent) + cls.exec_() + text = cls.lipsumText + cls.deleteLater() + return text + ## - # Slots + # Private Slots ## - def _doInsert(self): - """Load the text and insert it in the open document. - """ + @pyqtSlot() + def _doInsert(self) -> None: + """Generate the text.""" lipsumFile = CONFIG.assetPath("text") / "lipsum.txt" lipsumText = readTextFile(lipsumFile).splitlines() - if self.randSwitch.isChecked(): random.shuffle(lipsumText) - pCount = self.paraCount.value() - inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" - - self.mainGui.docEditor.insertText(inText) - - return - - def _doClose(self): - """Close the dialog window without doing anything. - """ + self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" self.close() return diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 3decd7308..d7a8096ec 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -344,8 +344,6 @@ def _getSelectedFormat(self) -> nwBuildFmt | None: def _saveSettings(self): """Save the user GUI settings.""" - logger.debug("Saving GuiManuscriptBuild settings") - winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) @@ -353,6 +351,7 @@ def _saveSettings(self): fmtWidth = CONFIG.rpxInt(mainSplit[0]) sumWidth = CONFIG.rpxInt(mainSplit[1]) + logger.debug("Saving State: GuiManuscriptBuild") pOptions = SHARED.project.options pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth) pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index e166ffbbc..5f7b68566 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -417,8 +417,6 @@ def _getSelectedBuild(self) -> BuildSettings | None: def _saveSettings(self): """Save the user GUI settings.""" - logger.debug("Saving GuiManuscript settings") - buildOrder = [] for i in range(self.buildList.count()): if item := self.buildList.item(i): @@ -442,6 +440,7 @@ def _saveSettings(self): detailsWidth = CONFIG.rpxInt(self.buildDetails.getColumnWidth()) detailsExpanded = self.buildDetails.getExpandedState() + logger.debug("Saving State: GuiManuscript") pOptions = SHARED.project.options pOptions.setValue("GuiManuscript", "winWidth", winWidth) pOptions.setValue("GuiManuscript", "winHeight", winHeight) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 952354242..e05251447 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -253,13 +253,11 @@ def _askToSaveBuild(self) -> None: def _saveSettings(self) -> None: """Save the various user settings.""" - logger.debug("Saving GuiBuildSettings settings") - winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) - treeWidth, filterWidth = self.optTabSelect.mainSplitSizes() + logger.debug("Saving State: GuiBuildSettings") pOptions = SHARED.project.options pOptions.setValue("GuiBuildSettings", "winWidth", winWidth) pOptions.setValue("GuiBuildSettings", "winHeight", winHeight) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index e8e4edef1..2100dc14c 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -29,7 +29,7 @@ from datetime import datetime from typing import TYPE_CHECKING -from PyQt5.QtGui import QPixmap, QCursor +from PyQt5.QtGui import QCloseEvent, QPixmap, QCursor from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, @@ -307,9 +307,20 @@ def populateGUI(self) -> None: return ## - # Slots + # Events ## + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() def _doClose(self) -> None: """Save the state of the window, clear cache, end close.""" self.logData = [] @@ -330,6 +341,7 @@ def _doClose(self) -> None: showIdleTime = self.showIdleTime.isChecked() histMax = self.histMax.value() + logger.debug("Saving State: GuiWritingStats") pOptions = SHARED.project.options pOptions.setValue("GuiWritingStats", "winWidth", winWidth) pOptions.setValue("GuiWritingStats", "winHeight", winHeight) @@ -347,6 +359,7 @@ def _doClose(self) -> None: pOptions.setValue("GuiWritingStats", "showIdleTime", showIdleTime) pOptions.setValue("GuiWritingStats", "histMax", histMax) pOptions.saveSettings() + self.close() return diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 01645e04f..c30ec7d35 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -83,7 +83,7 @@ def testBaseInit_Options(monkeypatch, fncPath): """Test command line options for logging level.""" monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr(sys, "argv", [ - "novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}" + "novelWriter.py", "--testmode", "--meminfo", f"--config={fncPath}", f"--data={fncPath}" ]) # Defaults w/None Args diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index 36ba5d42a..2587a8d54 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -35,7 +35,7 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): """Test the novelWriter about dialogs.""" # NW About - assert nwGUI.showAboutNWDialog(showNotes=True) is True + nwGUI.showAboutNWDialog(showNotes=True) qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) msgAbout = getGuiItem("GuiAbout") @@ -56,14 +56,7 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): msgAbout.showReleaseNotes() assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes - msgAbout._doClose() - - # Open Again from Menu - nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) - msgAbout = getGuiItem("GuiAbout") - assert msgAbout is not None - msgAbout._doClose() + msgAbout.close() # END Test testDlgAbout_NWDialog diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index ad0c338a9..239a45c57 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -45,12 +45,11 @@ def testDlgOther_QuoteSelect(qtbot, nwGUI): lastItem = anItem.text()[2] assert nwQuot.previewLabel.text() == lastItem - nwQuot._doAccept() + nwQuot.accept() assert nwQuot.result() == QDialog.Accepted assert nwQuot.selectedQuote == lastItem # qtbot.stop() - nwQuot._doReject() nwQuot.close() # END Test testDlgOther_QuoteSelect @@ -89,7 +88,7 @@ def mockUrlopenB(*a, **k): nwGUI.mainMenu.aUpdates.activate(QAction.Trigger) # qtbot.stop() - nwUpdate._doClose() + nwUpdate.close() # END Test testDlgOther_Updates @@ -101,13 +100,13 @@ def testDlgOther_EditLabel(qtbot, monkeypatch): with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Accepted) - newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") # type: ignore assert dlgOk is True assert newLabel == "Hello World" with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Rejected) - newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") # type: ignore assert dlgOk is False assert newLabel == "Hello World" diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index ae4d34aee..31d29c2c2 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -45,23 +45,12 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - with monkeypatch.context() as mp: - mp.setattr(GuiPreferences, "updateTheme", lambda *a: True) - mp.setattr(GuiPreferences, "updateSyntax", lambda *a: True) - mp.setattr(GuiPreferences, "needsRestart", lambda *a: True) - mp.setattr(GuiPreferences, "refreshTree", lambda *a: True) - nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) - + nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) nwPrefs = getGuiItem("GuiPreferences") assert isinstance(nwPrefs, GuiPreferences) nwPrefs.show() - assert nwPrefs.updateTheme is False - assert nwPrefs.updateSyntax is False - assert nwPrefs.needsRestart is False - assert nwPrefs.refreshTree is False - # General Settings qtbot.wait(KEY_DELAY) tabGeneral = nwPrefs.tabGeneral @@ -100,8 +89,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): qtbot.mouseClick(tabProjects.backupOnClose, Qt.LeftButton) assert tabProjects.backupOnClose.isChecked() - # qtbot.stop() - # Check Browse button monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "") assert not tabProjects._backupFolder() @@ -204,7 +191,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): # Save and Check Config qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) - nwPrefs._doClose() assert CONFIG.saveConfig() projFile = tstPaths.cnfDir / "novelwriter.conf" diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py index 8ea284435..c8617c72b 100644 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ b/tests/test_dialogs/test_dlg_projdetails.py @@ -66,12 +66,12 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): tocTab = projDet.tabContents tocTree = tocTab.tocTree assert tocTree.topLevelItemCount() == 7 - assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" - assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" - assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" - assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" - assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" - assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" + assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore + assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore + assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore + assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore + assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore + assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore # Count Pages tocTab.wpValue.setValue(100) @@ -82,8 +82,8 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): thePages = ["1", "2", "1", "1", "11", "17", "0"] thePage = ["i", "ii", "1", "2", "3", "14", "31"] for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] + assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore + assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore tocTab.poValue.setValue(5) tocTab.dblValue.setChecked(True) @@ -92,8 +92,8 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): thePages = ["2", "2", "2", "2", "12", "18", "0"] thePage = ["i", "iii", "1", "3", "5", "17", "35"] for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] + assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore + assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore # Re-populate assert tocTab._currentRoot is None @@ -103,7 +103,7 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): # qtbot.stop() # Clean Up - projDet._doClose() + projDet.close() nwGUI.closeMain() # END Test testDlgProjDetails_Dialog diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 1413bb4a4..1af844d7d 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -57,26 +57,26 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000) - projEdit = getGuiItem("GuiProjectSettings") - assert isinstance(projEdit, GuiProjectSettings) - projEdit.show() - qtbot.addWidget(projEdit) + projSettings = getGuiItem("GuiProjectSettings") + assert isinstance(projSettings, GuiProjectSettings) + projSettings.show() + qtbot.addWidget(projSettings) # Switch Tabs - projEdit._focusTab(GuiProjectSettings.TAB_REPLACE) - assert projEdit._tabBox.currentWidget() == projEdit.tabReplace + projSettings._focusTab(GuiProjectSettings.TAB_REPLACE) + assert projSettings._tabBox.currentWidget() == projSettings.tabReplace - projEdit._focusTab(GuiProjectSettings.TAB_IMPORT) - assert projEdit._tabBox.currentWidget() == projEdit.tabImport + projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) + assert projSettings._tabBox.currentWidget() == projSettings.tabImport - projEdit._focusTab(GuiProjectSettings.TAB_STATUS) - assert projEdit._tabBox.currentWidget() == projEdit.tabStatus + projSettings._focusTab(GuiProjectSettings.TAB_STATUS) + assert projSettings._tabBox.currentWidget() == projSettings.tabStatus - projEdit._focusTab(GuiProjectSettings.TAB_MAIN) - assert projEdit._tabBox.currentWidget() == projEdit.tabMain + projSettings._focusTab(GuiProjectSettings.TAB_MAIN) + assert projSettings._tabBox.currentWidget() == projSettings.tabMain # Clean Up - projEdit._doClose() + projSettings.close() # qtbot.stop() # END Test testDlgProjSettings_Dialog @@ -93,10 +93,10 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR CONFIG.setBackupPath(fncPath) # Set some values - theProject = SHARED.project - theProject.data.setSpellLang("en") - theProject.data.setAuthor("Jane Smith") - theProject.data.setAutoReplace({"A": "B", "C": "D"}) + project = SHARED.project + project.data.setSpellLang("en") + project.data.setAuthor("Jane Smith") + project.data.setAutoReplace({"A": "B", "C": "D"}) # Create Dialog projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) @@ -130,12 +130,13 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR assert tabMain.editAuthor.text() == "Jane Doe" projSettings._doSave() - assert theProject.data.name == "Project Name" - assert theProject.data.title == "Project Title" - assert theProject.data.author == "Jane Doe" + assert project.data.name == "Project Name" + assert project.data.title == "Project Title" + assert project.data.author == "Jane Doe" + + nwGUI._processProjectSettingsChanges() + assert nwGUI.windowTitle() == "novelWriter - Project Name" - # Clean up - projSettings._doClose() # qtbot.stop() # END Test testDlgProjSettings_Main @@ -334,9 +335,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat assert importItems[C.iMain]["name"] == "Main" assert importItems["i000014"]["name"] == "Final" - # Clean up # qtbot.stop() - projSettings._doClose() # END Test testDlgProjSettings_StatusImport @@ -422,8 +421,6 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mo "A": "B", "C": "D", "This": "With This Stuff" } - # Clean up # qtbot.stop() - projSettings._doClose() # END Test testDlgProjSettings_Replace diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index e775baa00..4c4ec9faa 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -68,11 +68,11 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath): wList._loadWordList() # Check that the content was loaded - assert wList.listBox.item(0).text() == "word_a" - assert wList.listBox.item(1).text() == "word_b" - assert wList.listBox.item(2).text() == "word_c" - assert wList.listBox.item(3).text() == "word_f" - assert wList.listBox.item(4).text() == "word_g" + assert wList.listBox.item(0).text() == "word_a" # type: ignore + assert wList.listBox.item(1).text() == "word_b" # type: ignore + assert wList.listBox.item(2).text() == "word_c" # type: ignore + assert wList.listBox.item(3).text() == "word_f" # type: ignore + assert wList.listBox.item(4).text() == "word_g" # type: ignore assert wList.listBox.count() == 5 # Add a blank word, which is ignored @@ -91,27 +91,27 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath): assert wList.listBox.count() == 6 # Check that the content now - assert wList.listBox.item(0).text() == "word_a" - assert wList.listBox.item(1).text() == "word_b" - assert wList.listBox.item(2).text() == "word_c" - assert wList.listBox.item(3).text() == "word_d" - assert wList.listBox.item(4).text() == "word_f" - assert wList.listBox.item(5).text() == "word_g" + assert wList.listBox.item(0).text() == "word_a" # type: ignore + assert wList.listBox.item(1).text() == "word_b" # type: ignore + assert wList.listBox.item(2).text() == "word_c" # type: ignore + assert wList.listBox.item(3).text() == "word_d" # type: ignore + assert wList.listBox.item(4).text() == "word_f" # type: ignore + assert wList.listBox.item(5).text() == "word_g" # type: ignore # Delete a word wList.newEntry.setText("delete_me") wList._doAdd() - assert wList.listBox.item(0).text() == "delete_me" + assert wList.listBox.item(0).text() == "delete_me" # type: ignore delItem = wList.listBox.findItems("delete_me", Qt.MatchExactly)[0] assert delItem.text() == "delete_me" delItem.setSelected(True) wList._doDelete() assert wList.listBox.findItems("delete_me", Qt.MatchExactly) == [] - assert wList.listBox.item(0).text() == "word_a" + assert wList.listBox.item(0).text() == "word_a" # type: ignore # Save files - assert wList._doSave() + wList._doSave() userDict.load() assert len(list(userDict)) == 6 assert "word_a" in userDict @@ -122,6 +122,5 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath): assert "word_g" in userDict # qtbot.stop() - wList._doClose() # END Test testDlgWordList_Dialog diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 659b44ee3..a43e86e80 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -29,6 +29,7 @@ C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile ) +from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QMenu, QMessageBox, QInputDialog @@ -62,11 +63,6 @@ def testGuiMain_ProjectBlocker(nwGUI): assert nwGUI.openSelectedItem() is False assert nwGUI.editItemLabel() is False assert nwGUI.rebuildIndex() is False - assert nwGUI.showProjectSettingsDialog() is False - assert nwGUI.showProjectDetailsDialog() is False - assert nwGUI.showBuildManuscriptDialog() is False - assert nwGUI.showProjectWordListDialog() is False - assert nwGUI.showWritingStatsDialog() is False # END Test testGuiMain_ProjectBlocker @@ -203,6 +199,28 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # END Test testGuiMain_ProjectTreeItems +@pytest.mark.gui +def testGuiMain_UpdateTheme(qtbot, nwGUI): + """Test updating the theme in the GUI.""" + mainTheme = SHARED.theme + CONFIG.guiTheme = "default_dark" + CONFIG.guiSyntax = "default_dark" + mainTheme.loadTheme() + mainTheme.loadSyntax() + nwGUI._processConfigChanges(True, True, True, True) + + syntaxBack = QColor(*SHARED.theme.colBack) + + assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack + assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack + assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntaxBack + assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack + + # qtbot.stop() + +# END Test testGuiMain_UpdateTheme + + @pytest.mark.gui def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): """Test the document editor.""" diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 9bc452d79..e660c02ce 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -23,7 +23,6 @@ import pytest from pathlib import Path -from configparser import ConfigParser from mocked import causeOSError from tools import writeFile @@ -33,6 +32,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType +from novelwriter.common import NWConfigParser from novelwriter.constants import nwLabels @@ -87,7 +87,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): # Parse Colours # ============= - parser = ConfigParser() + parser = NWConfigParser() parser["Palette"] = { "colour1": "100, 150, 200", "colour2": "100, 150, 200, 250", diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 191608515..f5199c6e3 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -30,43 +30,39 @@ @pytest.mark.gui -def testToolLipsum_Main(qtbot, nwGUI, projPath, mockRnd): - """Test the Lorem Ipsum tool. - """ +def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the Lorem Ipsum tool.""" # Check that we cannot open when there is no project nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger) assert getGuiItem("GuiLipsum") is None - # Create a new project buildTestProject(nwGUI, projPath) - assert nwGUI.openDocument(C.hSceneDoc) is True - assert len(nwGUI.docEditor.getText()) == 15 - - # Open the tool - nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiLipsum") is not None, timeout=1000) + nwLipsum = GuiLipsum(nwGUI) - nwLipsum = getGuiItem("GuiLipsum") - assert isinstance(nwLipsum, GuiLipsum) - - # Insert paragraphs + # Generate paragraphs nwGUI.docEditor.setCursorPosition(100) # End of document nwLipsum.paraCount.setValue(2) nwLipsum._doInsert() - theText = nwGUI.docEditor.getText() - assert "Lorem ipsum" in theText - assert len(theText) == 965 + assert "Lorem ipsum" in nwLipsum.lipsumText - # Insert random paragraph + # Generate random paragraph nwGUI.docEditor.setCursorPosition(1000) # End of document nwLipsum.randSwitch.setChecked(True) nwLipsum.paraCount.setValue(1) nwLipsum._doInsert() - theText = nwGUI.docEditor.getText() - assert len(theText) > 965 + assert len(nwLipsum.lipsumText) > 0 + + nwLipsum.setObjectName("") + nwLipsum.close() - # Close - nwLipsum._doClose() + # Trigger insertion in document + assert nwGUI.openDocument(C.hSceneDoc) is True + nwGUI.docEditor.setCursorLine(3) + with monkeypatch.context() as mp: + mp.setattr(GuiLipsum, "exec_", lambda *a: None) + mp.setattr(GuiLipsum, "lipsumText", "FooBar") + nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == "### New Scene\n\nFooBar" # qtbot.stop() diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index d734a117a..d25abf922 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -30,7 +30,7 @@ from mocked import causeOSError from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtWidgets import QDialogButtonBox +from PyQt5.QtWidgets import QAction, QDialogButtonBox from PyQt5.QtPrintSupport import QPrintPreviewDialog from novelwriter import CONFIG, SHARED @@ -49,9 +49,11 @@ def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Pat SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi") allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *" - manus = GuiManuscript(nwGUI) + nwGUI.mainMenu.aBuildManuscript.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiManuscript") is not None, timeout=1000) + manus = getGuiItem("GuiManuscript") + assert isinstance(manus, GuiManuscript) manus.show() - manus.loadContent() assert manus.docPreview.toPlainText().strip() == "" # Run the default build @@ -79,7 +81,6 @@ def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Pat assert manus.docPreview.toPlainText().strip() == "" manus.close() - # Finish # qtbot.stop() # END Test testManuscript_Init