From f7cdc7da0c20eb8918cc6fb24cf19637e45739fc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 27 May 2024 22:53:30 +0200 Subject: [PATCH 1/7] Add a dialog parent class that handles escape key presses --- novelwriter/dialogs/docmerge.py | 7 ++++--- novelwriter/dialogs/docsplit.py | 7 ++++--- novelwriter/dialogs/editlabel.py | 3 ++- novelwriter/dialogs/preferences.py | 19 +++++++------------ novelwriter/dialogs/projectsettings.py | 6 +++--- novelwriter/dialogs/quotes.py | 3 ++- novelwriter/dialogs/wordlist.py | 6 +++--- novelwriter/extensions/modified.py | 16 +++++++++++++--- novelwriter/tools/lipsum.py | 7 ++++--- novelwriter/tools/manusbuild.py | 6 +++--- novelwriter/tools/welcome.py | 8 ++++---- 11 files changed, 49 insertions(+), 39 deletions(-) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index d258c89a6..636e2de00 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -29,19 +29,20 @@ from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel, - QListWidget, QListWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QDialogButtonBox, QGridLayout, QLabel, QListWidget, + QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole logger = logging.getLogger(__name__) -class GuiDocMerge(QDialog): +class GuiDocMerge(NDialog): D_HANDLE = QtUserRole diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 49325a706..1a25144cc 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -29,19 +29,20 @@ from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QGridLayout, - QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QLabel, + QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtDialogCancel, QtDialogOk, QtUserRole logger = logging.getLogger(__name__) -class GuiDocSplit(QDialog): +class GuiDocSplit(NDialog): LINE_ROLE = QtUserRole LEVEL_ROLE = QtUserRole + 1 diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 2afbd6faa..956327012 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -31,12 +31,13 @@ ) from novelwriter import CONFIG +from novelwriter.extensions.modified import NDialog from novelwriter.types import QtDialogCancel, QtDialogOk logger = logging.getLogger(__name__) -class GuiEditLabel(QDialog): +class GuiEditLabel(NDialog): def __init__(self, parent: QWidget, text: str = "") -> None: super().__init__(parent=parent) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 26ea5a3f1..ccbe4cb0e 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -27,10 +27,10 @@ import logging from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QKeyEvent, QKeySequence +from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractButton, QApplication, QCompleter, QDialog, QDialogButtonBox, - QFileDialog, QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget + QAbstractButton, QApplication, QCompleter, QDialogButtonBox, QFileDialog, + QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -38,7 +38,9 @@ from novelwriter.constants import nwConst, nwUnicode from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm -from novelwriter.extensions.modified import NComboBox, NDoubleSpinBox, NIconToolButton, NSpinBox +from novelwriter.extensions.modified import ( + NComboBox, NDialog, NDoubleSpinBox, NIconToolButton, NSpinBox +) from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.types import ( @@ -49,7 +51,7 @@ logger = logging.getLogger(__name__) -class GuiPreferences(QDialog): +class GuiPreferences(NDialog): newPreferencesReady = pyqtSignal(bool, bool, bool, bool) @@ -772,13 +774,6 @@ def closeEvent(self, event: QCloseEvent) -> None: self.deleteLater() return - def keyPressEvent(self, event: QKeyEvent) -> None: - """Overload keyPressEvent to block enter key to save.""" - if event.matches(QKeySequence.StandardKey.Cancel): - self.close() - event.ignore() - return - ## # Private Slots ## diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 74b444099..2af8f3cbf 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtGui import QCloseEvent, QColor from PyQt5.QtWidgets import ( - QAbstractItemView, QApplication, QColorDialog, QDialog, QDialogButtonBox, + QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox, QHBoxLayout, QLineEdit, QMenu, QStackedWidget, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) @@ -40,7 +40,7 @@ from novelwriter.core.status import NWStatus, StatusEntry from novelwriter.enum import nwStatusShape from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm -from novelwriter.extensions.modified import NComboBox, NIconToolButton +from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.types import ( @@ -51,7 +51,7 @@ logger = logging.getLogger(__name__) -class GuiProjectSettings(QDialog): +class GuiProjectSettings(NDialog): PAGE_SETTINGS = 0 PAGE_STATUS = 1 diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 760e82d64..b0f944b0f 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -34,12 +34,13 @@ from novelwriter import CONFIG from novelwriter.constants import nwQuotes, trConst +from novelwriter.extensions.modified import NDialog from novelwriter.types import QtAlignCenter, QtAlignTop, QtDialogCancel, QtDialogOk, QtUserRole logger = logging.getLogger(__name__) -class GuiQuoteSelect(QDialog): +class GuiQuoteSelect(NDialog): _selected = "" diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index fa2b573c7..817781ee3 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractItemView, QApplication, QDialog, QDialogButtonBox, QFileDialog, + QAbstractItemView, QApplication, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit, QListWidget, QVBoxLayout, QWidget ) @@ -38,13 +38,13 @@ from novelwriter.common import formatFileFilter from novelwriter.core.spellcheck import UserDictionary from novelwriter.extensions.configlayout import NColourLabel -from novelwriter.extensions.modified import NIconToolButton +from novelwriter.extensions.modified import NDialog, NIconToolButton from novelwriter.types import QtDialogClose, QtDialogSave logger = logging.getLogger(__name__) -class GuiWordList(QDialog): +class GuiWordList(NDialog): newWordListReady = pyqtSignal() diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 210d9638b..486941060 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -31,7 +31,7 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import QWheelEvent +from PyQt5.QtGui import QKeyEvent, QKeySequence, QWheelEvent from PyQt5.QtWidgets import ( QApplication, QComboBox, QDialog, QDoubleSpinBox, QSpinBox, QToolButton, QWidget @@ -43,7 +43,17 @@ from novelwriter.guimain import GuiMain -class NToolDialog(QDialog): +class NDialog(QDialog): + + def keyPressEvent(self, event: QKeyEvent) -> None: + """Overload keyPressEvent and forward escape to close.""" + if event.matches(QKeySequence.StandardKey.Cancel): + self.close() + event.ignore() + return + + +class NToolDialog(NDialog): def __init__(self, parent: GuiMain) -> None: super().__init__(parent=parent) @@ -62,7 +72,7 @@ def activateDialog(self) -> None: return -class NNonBlockingDialog(QDialog): +class NNonBlockingDialog(NDialog): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent=parent) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 19b4cac96..5956db4bb 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -28,19 +28,20 @@ from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QSpinBox, - QVBoxLayout, QWidget + QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QSpinBox, QVBoxLayout, + QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.common import readTextFile +from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtAlignLeft, QtAlignRight, QtDialogClose, QtRoleAction logger = logging.getLogger(__name__) -class GuiLipsum(QDialog): +class GuiLipsum(NDialog): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 0af6ef52b..e12a6077a 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import QTimer, pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog, + QAbstractButton, QAbstractItemView, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSplitter, QVBoxLayout, QWidget ) @@ -42,14 +42,14 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.item import NWItem from novelwriter.enum import nwBuildFmt -from novelwriter.extensions.modified import NIconToolButton +from novelwriter.extensions.modified import NDialog, NIconToolButton from novelwriter.extensions.simpleprogress import NProgressSimple from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole logger = logging.getLogger(__name__) -class GuiManuscriptBuild(QDialog): +class GuiManuscriptBuild(NDialog): """GUI Tools: Manuscript Build Dialog This is the tool for running the build itself. It can be accessed diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index bb2914414..c7726b6ff 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -34,8 +34,8 @@ ) from PyQt5.QtGui import QCloseEvent, QColor, QFont, QPainter, QPaintEvent, QPen from PyQt5.QtWidgets import ( - QAction, QApplication, QDialog, QFileDialog, QFormLayout, QHBoxLayout, - QLabel, QLineEdit, QListView, QMenu, QPushButton, QScrollArea, QShortcut, + QAction, QApplication, QFileDialog, QFormLayout, QHBoxLayout, QLabel, + QLineEdit, QListView, QMenu, QPushButton, QScrollArea, QShortcut, QStackedWidget, QStyledItemDelegate, QStyleOptionViewItem, QVBoxLayout, QWidget ) @@ -46,7 +46,7 @@ from novelwriter.core.coretools import ProjectBuilder from novelwriter.enum import nwItemClass from novelwriter.extensions.configlayout import NWrappedWidgetBox -from novelwriter.extensions.modified import NIconToolButton, NSpinBox +from novelwriter.extensions.modified import NDialog, NIconToolButton, NSpinBox from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.versioninfo import VersionInfoWidget from novelwriter.types import QtAlignLeft, QtAlignRightTop, QtSelected @@ -56,7 +56,7 @@ PANEL_ALPHA = 178 -class GuiWelcome(QDialog): +class GuiWelcome(NDialog): openProjectRequest = pyqtSignal(Path) From b33bf954bbfedcfdbab612cae1dff3dac7409590 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 27 May 2024 23:36:17 +0200 Subject: [PATCH 2/7] Convert split and merge dialogs into class method dialogs --- novelwriter/dialogs/docmerge.py | 23 +++++++++-------- novelwriter/dialogs/docsplit.py | 23 +++++++++-------- novelwriter/dialogs/editlabel.py | 9 +++---- novelwriter/dialogs/quotes.py | 9 ++++--- novelwriter/gui/projtree.py | 42 ++++++++++++-------------------- novelwriter/types.py | 5 +++- 6 files changed, 51 insertions(+), 60 deletions(-) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 636e2de00..feead3940 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -27,7 +27,6 @@ import logging from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( QAbstractItemView, QDialogButtonBox, QGridLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget @@ -37,7 +36,7 @@ from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole +from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole logger = logging.getLogger(__name__) @@ -118,7 +117,7 @@ def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocMerge") return - def getData(self) -> dict: + def data(self) -> dict: """Return the user's choices.""" finalItems = [] for i in range(self.listBox.count()): @@ -131,15 +130,15 @@ def getData(self) -> dict: return self._data - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - event.accept() - self.deleteLater() - return + @classmethod + def getData(cls, parent: QWidget, handle: str, items: list[str]) -> tuple[dict, bool]: + """Pop the dialog and return the result.""" + cls = GuiDocMerge(parent, handle, items) + cls.exec() + data = cls.data() + accepted = cls.result() == QtAccepted + cls.deleteLater() + return data, accepted ## # Private Slots diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 1a25144cc..56f87880f 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -27,7 +27,6 @@ import logging from PyQt5.QtCore import pyqtSlot -from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget @@ -37,7 +36,7 @@ from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtDialogCancel, QtDialogOk, QtUserRole +from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtUserRole logger = logging.getLogger(__name__) @@ -146,7 +145,7 @@ def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocSplit") return - def getData(self) -> tuple[dict, list]: + def data(self) -> tuple[dict, list[str]]: """Return the user's choices. Also save the users options for the next time the dialog is used. """ @@ -179,15 +178,15 @@ def getData(self) -> tuple[dict, list]: return self._data, self._text - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - event.accept() - self.deleteLater() - return + @classmethod + def getData(cls, parent: QWidget, handle: str) -> tuple[dict, list[str], bool]: + """Pop the dialog and return the result.""" + cls = GuiDocSplit(parent, handle) + cls.exec() + data, text = cls.data() + accepted = cls.result() == QtAccepted + cls.deleteLater() + return data, text, accepted ## # Private Slots diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 956327012..05859a27d 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -25,14 +25,11 @@ import logging -from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, - QWidget -) +from PyQt5.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget from novelwriter import CONFIG from novelwriter.extensions.modified import NDialog -from novelwriter.types import QtDialogCancel, QtDialogOk +from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk logger = logging.getLogger(__name__) @@ -92,6 +89,6 @@ def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]: cls = GuiEditLabel(parent, text=text) cls.exec() label = cls.itemLabel - accepted = cls.result() == QDialog.DialogCode.Accepted + accepted = cls.result() == QtAccepted cls.deleteLater() return label, accepted diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index b0f944b0f..85289f3cc 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -28,14 +28,17 @@ from PyQt5.QtCore import QSize, pyqtSlot from PyQt5.QtGui import QFontMetrics from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, + QDialogButtonBox, QFrame, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG from novelwriter.constants import nwQuotes, trConst from novelwriter.extensions.modified import NDialog -from novelwriter.types import QtAlignCenter, QtAlignTop, QtDialogCancel, QtDialogOk, QtUserRole +from novelwriter.types import ( + QtAccepted, QtAlignCenter, QtAlignTop, QtDialogCancel, QtDialogOk, + QtUserRole +) logger = logging.getLogger(__name__) @@ -127,7 +130,7 @@ def getQuote(cls, parent: QWidget, current: str = "") -> tuple[str, bool]: cls = GuiQuoteSelect(parent, current=current) cls.exec() quote = cls._selected - accepted = cls.result() == QDialog.DialogCode.Accepted + accepted = cls.result() == QtAccepted cls.deleteLater() return quote, accepted diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 23806eea2..88a54e7f3 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -34,9 +34,8 @@ from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, - QLabel, QMenu, QShortcut, QTreeWidget, QTreeWidgetItem, QVBoxLayout, - QWidget + QAbstractItemView, QAction, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QShortcut, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -1397,14 +1396,10 @@ def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool: if not newFile: itemList.remove(tHandle) - dlgMerge = GuiDocMerge(SHARED.mainGui, tHandle, itemList) - dlgMerge.exec() - - if dlgMerge.result() == QDialog.DialogCode.Accepted: - - mrgData = dlgMerge.getData() - mrgList = mrgData.get("finalItems", []) - if not mrgList: + data, status = GuiDocMerge.getData(SHARED.mainGui, tHandle, itemList) + if status: + items = data.get("finalItems", []) + if not items: SHARED.info(self.tr("No documents selected for merging.")) return False @@ -1424,7 +1419,7 @@ def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool: else: return False - for sHandle in mrgList: + for sHandle in items: docMerger.appendText(sHandle, True, mLabel) if not docMerger.writeTargetDoc(): @@ -1441,8 +1436,8 @@ def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool: self.projView.openDocumentRequest.emit(mHandle, nwDocMode.EDIT, "", False) self.projView.setSelectedHandle(mHandle, doScroll=True) - if mrgData.get("moveToTrash", False): - for sHandle in reversed(mrgData.get("finalItems", [])): + if data.get("moveToTrash", False): + for sHandle in reversed(data.get("finalItems", [])): trItem = self._getTreeItem(sHandle) if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0: self.moveItemToTrash(sHandle, askFirst=False, flush=False) @@ -1468,16 +1463,11 @@ def _splitDocument(self, tHandle: str) -> bool: logger.error("Only valid document items can be split") return False - dlgSplit = GuiDocSplit(SHARED.mainGui, tHandle) - dlgSplit.exec() - - if dlgSplit.result() == QDialog.DialogCode.Accepted: - - splitData, splitText = dlgSplit.getData() - - headerList = splitData.get("headerList", []) - intoFolder = splitData.get("intoFolder", False) - docHierarchy = splitData.get("docHierarchy", False) + data, text, status = GuiDocSplit.getData(SHARED.mainGui, tHandle) + if status: + headerList = data.get("headerList", []) + intoFolder = data.get("intoFolder", False) + docHierarchy = data.get("docHierarchy", False) docSplit = DocSplitter(SHARED.project, tHandle) if intoFolder: @@ -1487,7 +1477,7 @@ def _splitDocument(self, tHandle: str) -> bool: else: docSplit.setParentItem(tItem.itemParent) - docSplit.splitDocument(headerList, splitText) + docSplit.splitDocument(headerList, text) for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy): SHARED.project.index.reIndexHandle(dHandle) self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) @@ -1498,7 +1488,7 @@ def _splitDocument(self, tHandle: str) -> bool: info=docSplit.getError() ) - if splitData.get("moveToTrash", False): + if data.get("moveToTrash", False): self.moveItemToTrash(tHandle, askFirst=False, flush=True) self.saveTreeOrder() diff --git a/novelwriter/types.py b/novelwriter/types.py index 8cdfb1db7..e235c2a46 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import QRegularExpression, Qt from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat -from PyQt5.QtWidgets import QDialogButtonBox, QSizePolicy, QStyle +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QStyle # Qt Alignment Flags @@ -80,6 +80,9 @@ # Dialog Button Box Types +QtAccepted = QDialog.DialogCode.Accepted +QtRejected = QDialog.DialogCode.Rejected + QtDialogApply = QDialogButtonBox.StandardButton.Apply QtDialogCancel = QDialogButtonBox.StandardButton.Cancel QtDialogClose = QDialogButtonBox.StandardButton.Close From 087a9cdd2eb41236ba1f48709941830c637f11bd Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 27 May 2024 23:36:53 +0200 Subject: [PATCH 3/7] Update tests --- tests/test_dialogs/test_dlg_dialogs.py | 13 +++---- tests/test_dialogs/test_dlg_docmerge.py | 35 ++++++++++++++----- tests/test_dialogs/test_dlg_docsplit.py | 22 +++++++++--- .../test_dialogs/test_dlg_projectsettings.py | 6 ++-- tests/test_dialogs/test_dlg_wordlist.py | 5 +-- tests/test_gui/test_gui_projtree.py | 18 +++++----- 6 files changed, 68 insertions(+), 31 deletions(-) diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 68b28b95a..77de0832c 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -23,10 +23,11 @@ import pytest from PyQt5.QtCore import QItemSelectionModel -from PyQt5.QtWidgets import QDialog, QListWidgetItem +from PyQt5.QtWidgets import QListWidgetItem from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.quotes import GuiQuoteSelect +from novelwriter.types import QtAccepted, QtRejected @pytest.mark.gui @@ -47,17 +48,17 @@ def testDlgOther_QuoteSelect(qtbot, monkeypatch, nwGUI): assert nwQuot.previewLabel.text() == lastItem nwQuot.accept() - assert nwQuot.result() == QDialog.DialogCode.Accepted + assert nwQuot.result() == QtAccepted assert nwQuot.selectedQuote == lastItem nwQuot.close() # Test Class Method with monkeypatch.context() as mp: - mp.setattr(GuiQuoteSelect, "result", lambda *a: QDialog.DialogCode.Accepted) + mp.setattr(GuiQuoteSelect, "result", lambda *a: QtAccepted) assert GuiQuoteSelect.getQuote(nwGUI, current="X") == ("X", True) with monkeypatch.context() as mp: - mp.setattr(GuiQuoteSelect, "result", lambda *a: QDialog.DialogCode.Rejected) + mp.setattr(GuiQuoteSelect, "result", lambda *a: QtRejected) assert GuiQuoteSelect.getQuote(nwGUI, current="X") == ("X", False) # qtbot.stop() @@ -69,13 +70,13 @@ def testDlgOther_EditLabel(qtbot, monkeypatch): monkeypatch.setattr(GuiEditLabel, "exec", lambda *a: None) with monkeypatch.context() as mp: - mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.DialogCode.Accepted) + mp.setattr(GuiEditLabel, "result", lambda *a: QtAccepted) 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.DialogCode.Rejected) + mp.setattr(GuiEditLabel, "result", lambda *a: QtRejected) 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_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 3f85a13e3..acff24221 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -25,16 +25,15 @@ from PyQt5.QtCore import Qt from novelwriter.dialogs.docmerge import GuiDocMerge -from novelwriter.types import QtUserRole +from novelwriter.types import QtAccepted, QtRejected, QtUserRole from tests.tools import C, buildTestProject @pytest.mark.gui -def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd): - """Test the merge documents tool. - """ - # Create a new project +def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the merge documents tool.""" + monkeypatch.setattr(GuiDocMerge, "exec", lambda *a: None) buildTestProject(nwGUI, projPath) # Check that the dialog kan handle invalid items @@ -54,13 +53,16 @@ def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd): itemOne = nwMerge.listBox.item(0) itemTwo = nwMerge.listBox.item(1) + assert itemOne is not None + assert itemTwo is not None + assert itemOne.data(QtUserRole) == C.hChapterDoc assert itemTwo.data(QtUserRole) == C.hSceneDoc assert itemOne.checkState() == Qt.CheckState.Checked assert itemTwo.checkState() == Qt.CheckState.Checked - data = nwMerge.getData() + data = nwMerge.data() assert data["sHandle"] == C.hChapterDir assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc] assert data["moveToTrash"] is False @@ -70,7 +72,7 @@ def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd): itemTwo.setCheckState(Qt.CheckState.Unchecked) nwMerge.trashSwitch.setChecked(True) - data = nwMerge.getData() + data = nwMerge.data() assert data["sHandle"] == C.hChapterDir assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc] assert data["moveToTrash"] is True @@ -79,10 +81,27 @@ def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd): # Restore default values nwMerge._resetList() - data = nwMerge.getData() + data = nwMerge.data() assert data["sHandle"] == C.hChapterDir assert data["origItems"] == [C.hChapterDir, C.hChapterDoc, C.hSceneDoc] assert data["moveToTrash"] is True assert data["finalItems"] == [C.hChapterDoc, C.hSceneDoc] + # Test Class Method + with monkeypatch.context() as mp: + mp.setattr(GuiDocMerge, "result", lambda *a: QtAccepted) + data, status = GuiDocMerge.getData( + nwGUI, C.hChapterDir, [C.hChapterDir, C.hChapterDoc, C.hSceneDoc] + ) + assert data["sHandle"] == C.hChapterDir + assert status is True + + with monkeypatch.context() as mp: + mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected) + data, status = GuiDocMerge.getData( + nwGUI, C.hChapterDir, [C.hChapterDir, C.hChapterDoc, C.hSceneDoc] + ) + assert data["sHandle"] == C.hChapterDir + assert status is False + # qtbot.stop() diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index b0b4689d9..01ad84364 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -25,6 +25,7 @@ from novelwriter import SHARED from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel +from novelwriter.types import QtAccepted, QtRejected from tests.tools import C, buildTestProject @@ -33,8 +34,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the split document tool.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - # Create a new project + monkeypatch.setattr(GuiDocSplit, "exec", lambda *a: None) buildTestProject(nwGUI, projPath) project = SHARED.project @@ -74,7 +74,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwSplit.splitLevel.setCurrentIndex(3) assert nwSplit.listBox.count() == 12 - data, text = nwSplit.getData() + data, text = nwSplit.data() assert text == docText.splitlines() assert data["sHandle"] == hSplitDoc assert data["spLevel"] == 4 @@ -97,5 +97,19 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwSplit._loadContent(C.hNovelRoot) assert nwSplit.listBox.count() == 0 - nwSplit.reject() + # Test Class Method + with monkeypatch.context() as mp: + mp.setattr(GuiDocSplit, "result", lambda *a: QtAccepted) + data, text, status = GuiDocSplit.getData(nwGUI, hSplitDoc) + assert data["sHandle"] == hSplitDoc + assert text == docText.splitlines() + assert status is True + + with monkeypatch.context() as mp: + mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected) + data, text, status = GuiDocSplit.getData(nwGUI, hSplitDoc) + assert data["sHandle"] == hSplitDoc + assert text == docText.splitlines() + assert status is False + # qtbot.stop() diff --git a/tests/test_dialogs/test_dlg_projectsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py index 48be942b9..210012731 100644 --- a/tests/test_dialogs/test_dlg_projectsettings.py +++ b/tests/test_dialogs/test_dlg_projectsettings.py @@ -23,13 +23,13 @@ import pytest from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QAction, QColorDialog, QDialog +from PyQt5.QtWidgets import QAction, QColorDialog from novelwriter import CONFIG, SHARED from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import nwItemType, nwStatusShape -from novelwriter.types import QtMouseLeft +from novelwriter.types import QtAccepted, QtMouseLeft from tests.tools import C, buildTestProject @@ -43,7 +43,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): """ # Block the GUI blocking thread monkeypatch.setattr(GuiProjectSettings, "exec", lambda *a: None) - monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QDialog.DialogCode.Accepted) + monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QtAccepted) # Check that we cannot open when there is no project nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 085de7b2d..636c35d6e 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -23,11 +23,12 @@ import pytest from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QAction, QDialog, QFileDialog +from PyQt5.QtWidgets import QAction, QFileDialog from novelwriter import SHARED from novelwriter.core.spellcheck import UserDictionary from novelwriter.dialogs.wordlist import GuiWordList +from novelwriter.types import QtAccepted from tests.mocked import causeOSError from tests.tools import buildTestProject @@ -39,7 +40,7 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncPath, projPath): buildTestProject(nwGUI, projPath) monkeypatch.setattr(GuiWordList, "exec", lambda *a: None) - monkeypatch.setattr(GuiWordList, "result", lambda *a: QDialog.DialogCode.Accepted) + monkeypatch.setattr(GuiWordList, "result", lambda *a: QtAccepted) monkeypatch.setattr(GuiWordList, "accept", lambda *a: None) # Open project diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 7a1ca6fa1..12402fba2 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import QEvent, QMimeData, QPoint, Qt, QTimer from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent -from PyQt5.QtWidgets import QDialog, QMenu, QMessageBox, QTreeWidget, QTreeWidgetItem +from PyQt5.QtWidgets import QMenu, QMessageBox, QTreeWidget, QTreeWidgetItem from novelwriter import CONFIG, SHARED from novelwriter.core.item import NWItem @@ -37,7 +37,7 @@ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwWidget from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu from novelwriter.guimain import GuiMain -from novelwriter.types import QtModeNone, QtMouseLeft, QtMouseMiddle +from novelwriter.types import QtAccepted, QtModeNone, QtMouseLeft, QtMouseMiddle, QtRejected from tests.mocked import causeOSError from tests.tools import C, buildTestProject @@ -529,8 +529,9 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocMerge, "__init__", lambda *a: None) monkeypatch.setattr(GuiDocMerge, "exec", lambda *a: None) - monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QDialog.DialogCode.Accepted) - monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData) + monkeypatch.setattr(GuiDocMerge, "deleteLater", lambda *a: None) + monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted) + monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData) buildTestProject(nwGUI, projPath) @@ -585,7 +586,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # User cancels merge with monkeypatch.context() as mp: - mp.setattr(GuiDocMerge, "result", lambda *a: QDialog.DialogCode.Rejected) + mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected) assert projTree._mergeDocuments(hChapter1, True) is False # The merge goes through @@ -628,8 +629,9 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocSplit, "__init__", lambda *a: None) monkeypatch.setattr(GuiDocSplit, "exec", lambda *a: None) - monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QDialog.DialogCode.Accepted) - monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText)) + monkeypatch.setattr(GuiDocSplit, "deleteLater", lambda *a: None) + monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted) + monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText)) # Create a project buildTestProject(nwGUI, projPath) @@ -722,7 +724,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # Cancelled by user with monkeypatch.context() as mp: - mp.setattr(GuiDocSplit, "result", lambda *a: QDialog.DialogCode.Rejected) + mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected) assert projTree._splitDocument(hSplitDoc) is False # qtbot.stop() From b01a2f5abdba6b146f33bd724827520db10566fa Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 28 May 2024 21:15:32 +0200 Subject: [PATCH 4/7] Simplify the About dialog --- novelwriter/dialogs/about.py | 19 +++++++------------ novelwriter/guimain.py | 3 +-- tests/test_dialogs/test_dlg_about.py | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index ee138d375..a0b2a1c48 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -33,14 +33,14 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import cssCol, readTextFile from novelwriter.extensions.configlayout import NColourLabel -from novelwriter.extensions.modified import NNonBlockingDialog +from novelwriter.extensions.modified import NDialog from novelwriter.extensions.versioninfo import VersionInfoWidget from novelwriter.types import QtAlignRightTop, QtDialogClose logger = logging.getLogger(__name__) -class GuiAbout(NNonBlockingDialog): +class GuiAbout(NDialog): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -106,7 +106,9 @@ def __init__(self, parent: QWidget) -> None: self.setLayout(self.outerBox) self.setSizeGripEnabled(True) + self._setStyleSheet() + self._fillCreditsPage() logger.debug("Ready: GuiAbout") @@ -116,11 +118,6 @@ def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiAbout") return - def populateGUI(self) -> None: - """Populate tabs with text.""" - self._fillCreditsPage() - return - ## # Events ## @@ -137,16 +134,14 @@ def closeEvent(self, event: QCloseEvent) -> None: def _fillCreditsPage(self) -> None: """Load the content for the Credits page.""" - docPath = CONFIG.assetPath("text") / "credits_en.htm" - docText = readTextFile(docPath) - if docText: - self.txtCredits.setHtml(docText) + if html := readTextFile(CONFIG.assetPath("text") / "credits_en.htm"): + self.txtCredits.setHtml(html) else: self.txtCredits.setHtml("Error loading credits text ...") return def _setStyleSheet(self) -> None: - """Set stylesheet for all browser tabs.""" + """Set stylesheet text document.""" baseCol = cssCol(self.palette().window().color()) self.txtCredits.setStyleSheet( f"QTextBrowser {{border: none; background: {baseCol};}} " diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index eb9b4edab..0cfe156d4 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -827,8 +827,7 @@ def showWritingStatsDialog(self) -> None: def showAboutNWDialog(self) -> None: """Show the novelWriter about dialog.""" dialog = GuiAbout(self) - dialog.activateDialog() - dialog.populateGUI() + dialog.exec() return @pyqtSlot() diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index 01748dcda..ddc175bba 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -33,9 +33,9 @@ @pytest.mark.gui def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): """Test the novelWriter about dialogs.""" - # NW About - nwGUI.showAboutNWDialog() + monkeypatch.setattr(GuiAbout, "exec", lambda *a: None) + nwGUI.showAboutNWDialog() qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiAbout) is not None, timeout=1000) msgAbout = SHARED.findTopLevelWidget(GuiAbout) assert isinstance(msgAbout, GuiAbout) From 019dd3e478f413d15c2ec9c817d34a6f72e51155 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 28 May 2024 21:16:40 +0200 Subject: [PATCH 5/7] Change how dialogs are deleted --- novelwriter/dialogs/about.py | 2 +- novelwriter/dialogs/docmerge.py | 2 +- novelwriter/dialogs/docsplit.py | 2 +- novelwriter/dialogs/editlabel.py | 2 +- novelwriter/dialogs/preferences.py | 2 +- novelwriter/dialogs/projectsettings.py | 2 +- novelwriter/dialogs/quotes.py | 2 +- novelwriter/dialogs/wordlist.py | 2 +- novelwriter/extensions/modified.py | 9 +++++++++ novelwriter/tools/dictionaries.py | 2 +- novelwriter/tools/lipsum.py | 2 +- novelwriter/tools/manusbuild.py | 2 +- novelwriter/tools/manuscript.py | 2 +- novelwriter/tools/manussettings.py | 2 +- novelwriter/tools/noveldetails.py | 2 +- novelwriter/tools/welcome.py | 2 +- novelwriter/tools/writingstats.py | 2 +- tests/test_gui/test_gui_projtree.py | 4 ++-- 18 files changed, 27 insertions(+), 18 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index a0b2a1c48..226d0054f 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -125,7 +125,7 @@ def __del__(self) -> None: # pragma: no cover def closeEvent(self, event: QCloseEvent) -> None: """Capture the close event and perform cleanup.""" event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index feead3940..32b52a8d3 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -137,7 +137,7 @@ def getData(cls, parent: QWidget, handle: str, items: list[str]) -> tuple[dict, cls.exec() data = cls.data() accepted = cls.result() == QtAccepted - cls.deleteLater() + cls.softDelete() return data, accepted ## diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 56f87880f..5c4e586be 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -185,7 +185,7 @@ def getData(cls, parent: QWidget, handle: str) -> tuple[dict, list[str], bool]: cls.exec() data, text = cls.data() accepted = cls.result() == QtAccepted - cls.deleteLater() + cls.softDelete() return data, text, accepted ## diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 05859a27d..d7cf8875c 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -90,5 +90,5 @@ def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]: cls.exec() label = cls.itemLabel accepted = cls.result() == QtAccepted - cls.deleteLater() + cls.softDelete() return label, accepted diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index ccbe4cb0e..c723ba66a 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -771,7 +771,7 @@ def closeEvent(self, event: QCloseEvent) -> None: event.accept() QApplication.processEvents() self.done(nwConst.DLG_FINISHED) - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 2af8f3cbf..010f50e19 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -147,7 +147,7 @@ def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window and save settings.""" self._saveSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 85289f3cc..441a52b61 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -131,7 +131,7 @@ def getQuote(cls, parent: QWidget, current: str = "") -> tuple[str, bool]: cls.exec() quote = cls._selected accepted = cls.result() == QtAccepted - cls.deleteLater() + cls.softDelete() return quote, accepted ## diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 817781ee3..51ce49b60 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -140,7 +140,7 @@ def closeEvent(self, event: QCloseEvent) -> None: """Capture the close event and perform cleanup.""" self._saveGuiSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 486941060..120484970 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -45,6 +45,15 @@ class NDialog(QDialog): + def softDelete(self) -> None: + """Since calling deleteLater is sometimes not safe from Python, + as the C++ object can be deleted before the Python process is + done with the object, we instead set the dialog's parent to None + so that it gets garbage collected when it runs out of scope. + """ + self.setParent(None) # type: ignore + return + def keyPressEvent(self, event: QKeyEvent) -> None: """Overload keyPressEvent and forward escape to close.""" if event.matches(QKeySequence.StandardKey.Cancel): diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index d0e11b465..f56e57619 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -172,7 +172,7 @@ def initDialog(self) -> bool: def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window.""" event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 5956db4bb..9296d5b8a 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -133,7 +133,7 @@ def getLipsum(cls, parent: QWidget) -> str: cls = GuiLipsum(parent) cls.exec() text = cls.lipsumText - cls.deleteLater() + cls.softDelete() return text ## diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index e12a6077a..a8f2f7c2e 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -250,7 +250,7 @@ def closeEvent(self, event: QCloseEvent) -> None: """ self._saveSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 841eaa15a..54960270e 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -265,7 +265,7 @@ def closeEvent(self, event: QCloseEvent) -> None: if isinstance(obj, GuiBuildSettings) and obj.isVisible(): obj.close() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 1488c3b81..23b2a9469 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -200,7 +200,7 @@ def closeEvent(self, event: QEvent) -> None: self._askToSaveBuild() self._saveSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index dc2ac3c2d..9362c6406 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -150,7 +150,7 @@ def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window and save settings.""" self._saveSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index c7726b6ff..3f47b7078 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -196,7 +196,7 @@ def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window and save settings.""" self._saveSettings() event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index c0b630853..8e42a0c03 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -318,7 +318,7 @@ def populateGUI(self) -> None: def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window.""" event.accept() - self.deleteLater() + self.softDelete() return ## diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 12402fba2..c81a539b0 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -529,7 +529,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocMerge, "__init__", lambda *a: None) monkeypatch.setattr(GuiDocMerge, "exec", lambda *a: None) - monkeypatch.setattr(GuiDocMerge, "deleteLater", lambda *a: None) + monkeypatch.setattr(GuiDocMerge, "softDelete", lambda *a: None) monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted) monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData) @@ -629,7 +629,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, monkeypatch.setattr(GuiDocSplit, "__init__", lambda *a: None) monkeypatch.setattr(GuiDocSplit, "exec", lambda *a: None) - monkeypatch.setattr(GuiDocSplit, "deleteLater", lambda *a: None) + monkeypatch.setattr(GuiDocSplit, "softDelete", lambda *a: None) monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted) monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText)) From d8693c7afaa96f1ad6bb1535c1f44be9d62ad1a2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 28 May 2024 21:23:57 +0200 Subject: [PATCH 6/7] Remove deleteLater form message boxes, and some redundant ones in tests --- novelwriter/shared.py | 4 ---- tests/test_dialogs/test_dlg_preferences.py | 2 -- tests/test_tools/test_tools_welcome.py | 1 - 3 files changed, 7 deletions(-) diff --git a/novelwriter/shared.py b/novelwriter/shared.py index d6c19a9b5..fcd4ddd10 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -316,7 +316,6 @@ def info(self, text: str, info: str = "", details: str = "", log: bool = True) - if log: logger.info(self._lastAlert, stacklevel=2) alert.exec() - alert.deleteLater() return def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None: @@ -328,7 +327,6 @@ def warn(self, text: str, info: str = "", details: str = "", log: bool = True) - if log: logger.warning(self._lastAlert, stacklevel=2) alert.exec() - alert.deleteLater() return def error(self, text: str, info: str = "", details: str = "", log: bool = True, @@ -343,7 +341,6 @@ def error(self, text: str, info: str = "", details: str = "", log: bool = True, if log: logger.error(self._lastAlert, stacklevel=2) alert.exec() - alert.deleteLater() return def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: @@ -354,7 +351,6 @@ def question(self, text: str, info: str = "", details: str = "", warn: bool = Fa self._lastAlert = alert.logMessage alert.exec() isYes = alert.result() == QMessageBox.StandardButton.Yes - alert.deleteLater() return isYes ## diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 32d6a68b9..48fd08b79 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -91,7 +91,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): """Test the preferences dialog actions.""" monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - monkeypatch.setattr(GuiPreferences, "deleteLater", lambda *a: None) prefs = GuiPreferences(nwGUI) prefs.show() @@ -155,7 +154,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths): monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: spelling) monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages) - monkeypatch.setattr(GuiPreferences, "deleteLater", lambda *a: None) prefs = GuiPreferences(nwGUI) prefs.show() diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py index fd23ea95c..7afefb115 100644 --- a/tests/test_tools/test_tools_welcome.py +++ b/tests/test_tools/test_tools_welcome.py @@ -70,7 +70,6 @@ def testToolWelcome_Main(qtbot: QtBot, monkeypatch, nwGUI, fncPath): def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath): """Test the open tab in the Welcome window.""" monkeypatch.setattr(QMenu, "exec", lambda *a: None) - monkeypatch.setattr(QMenu, "deleteLater", lambda *a: None) CONFIG.recentProjects.update("/stuff/project_one", "Project One", 12345, 1690000000) CONFIG.recentProjects.update("/stuff/project_two", "Project Two", 54321, 1700000000) From 9b09b13bf955e2bcfa297095a9291457a53f0439 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 28 May 2024 21:43:09 +0200 Subject: [PATCH 7/7] Improve Manuscript dialog progress bar position --- novelwriter/tools/manuscript.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 54960270e..e0e959ee8 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -891,9 +891,8 @@ def _updateDocMargins(self) -> None: document within the viewport. """ vBar = self.verticalScrollBar() - sW = vBar.width() if vBar.isVisible() else 0 tB = self.frameWidth() - vW = self.width() - 2*tB - sW + vW = self.width() - 2*tB - vBar.width() vH = self.height() - 2*tB tH = self.ageLabel.height() pS = self.buildProgress.width()