diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 8317a30d..da6ba884 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -27,20 +27,20 @@ import logging from PyQt5.QtGui import QCloseEvent, QColor from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, - QWidget + QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget ) 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.versioninfo import VersionInfoWidget from novelwriter.types import QtAlignRightTop, QtDialogClose logger = logging.getLogger(__name__) -class GuiAbout(QDialog): +class GuiAbout(NNonBlockingDialog): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index b83a2662..8f185b24 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -6,6 +6,8 @@ File History: Created: 2024-02-01 [2.3b1] NComboBox Created: 2024-02-01 [2.3b1] NSpinBox Created: 2024-02-01 [2.3b1] NDoubleSpinBox +Created: 2024-05-01 [2.5b1] NToolDialog +Created: 2024-05-01 [2.5b1] NNonBlockingDialog This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -29,9 +31,52 @@ from enum import Enum from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QWheelEvent -from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QToolButton, QWidget +from PyQt5.QtWidgets import ( + QApplication, QComboBox, QDialog, QDoubleSpinBox, QSpinBox, QToolButton, + QWidget +) -from novelwriter import SHARED +from novelwriter import CONFIG, SHARED + + +class NToolDialog(QDialog): + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent=parent) + self.setModal(False) + if CONFIG.osDarwin: + self.setWindowFlag(Qt.WindowType.Tool) + return + + def activateDialog(self) -> None: + """Helper function to activate dialog on various systems.""" + self.show() + if CONFIG.osWindows: + self.activateWindow() + self.raise_() + QApplication.processEvents() + return + +# END Class NToolDialog + + +class NNonBlockingDialog(QDialog): + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent=parent) + self.setModal(True) + return + + def activateDialog(self) -> None: + """Helper function to activate dialog on various systems.""" + self.show() + if CONFIG.osWindows: + self.activateWindow() + self.raise_() + QApplication.processEvents() + return + +# END Class NNonBlockingDialog class NComboBox(QComboBox): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 105d2652..afe7966d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -23,18 +23,18 @@ along with this program. If not, see . """ from __future__ import annotations -import sys import logging +import sys -from time import time -from pathlib import Path from datetime import datetime +from pathlib import Path +from time import time from PyQt5.QtCore import Qt, QTimer, pyqtSlot from PyQt5.QtGui import QCloseEvent, QCursor, QIcon from PyQt5.QtWidgets import ( - QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut, QSplitter, - QStackedWidget, QVBoxLayout, QWidget + QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, + QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED, __hexversion__, __version__ @@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.dialogs.wordlist import GuiWordList -from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwWidget, nwView +from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwView, nwWidget from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer from novelwriter.gui.docviewerpanel import GuiDocViewerPanel @@ -794,10 +794,7 @@ class GuiMain(QMainWindow): """Open the novel details dialog.""" if SHARED.hasProject: dialog = GuiNovelDetails(self) - dialog.setModal(True) - dialog.show() - dialog.raise_() - QApplication.processEvents() + dialog.activateDialog() dialog.updateValues() return @@ -807,10 +804,7 @@ class GuiMain(QMainWindow): if SHARED.hasProject: if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None: dialog = GuiManuscript(self) - dialog.setModal(False) - dialog.show() - dialog.raise_() - QApplication.processEvents() + dialog.activateDialog() dialog.loadContent() return @@ -829,10 +823,7 @@ class GuiMain(QMainWindow): if SHARED.hasProject: if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None: dialog = GuiWritingStats(self) - dialog.setModal(False) - dialog.show() - dialog.raise_() - QApplication.processEvents() + dialog.activateDialog() dialog.populateGUI() return @@ -840,10 +831,7 @@ class GuiMain(QMainWindow): def showAboutNWDialog(self) -> None: """Show the novelWriter about dialog.""" dialog = GuiAbout(self) - dialog.setModal(True) - dialog.show() - dialog.raise_() - QApplication.processEvents() + dialog.activateDialog() dialog.populateGUI() return @@ -858,10 +846,7 @@ class GuiMain(QMainWindow): def showDictionariesDialog(self) -> None: """Show the download dictionaries dialog.""" dialog = GuiDictionaries(self) - dialog.setModal(True) - dialog.show() - dialog.raise_() - QApplication.processEvents() + dialog.activateDialog() if not dialog.initDialog(): dialog.close() SHARED.error(self.tr("Could not initialise the dialog.")) diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index a2607e63..982ebcc9 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -31,20 +31,20 @@ from zipfile import ZipFile from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QCloseEvent, QTextCursor from PyQt5.QtWidgets import ( - QApplication, QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, - QLabel, QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget + QApplication, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel, + QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED -from novelwriter.common import formatFileFilter, openExternalPath, formatInt, getFileSize +from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExternalPath from novelwriter.error import formatException -from novelwriter.extensions.modified import NIconToolButton +from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog from novelwriter.types import QtDialogClose logger = logging.getLogger(__name__) -class GuiDictionaries(QDialog): +class GuiDictionaries(NNonBlockingDialog): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 782a7109..c3dac96b 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -28,16 +28,15 @@ import logging from datetime import datetime from time import time -from typing import TYPE_CHECKING -from PyQt5.QtCore import QTimer, QUrl, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent -from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter +from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtWidgets import ( - QAbstractItemView, QApplication, QDialog, QFormLayout, QGridLayout, - QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, - QSplitter, QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout, + QLabel, QListWidget, QListWidgetItem, QPushButton, QSplitter, + QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -48,7 +47,7 @@ from novelwriter.core.tohtml import ToHtml from novelwriter.core.tokenizer import HeadingFormatter from novelwriter.error import logException from novelwriter.extensions.circularprogress import NProgressCircle -from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton +from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manussettings import GuiBuildSettings @@ -57,13 +56,10 @@ from novelwriter.types import ( QtSizeExpanding, QtSizeIgnored, QtUserRole ) -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) -class GuiManuscript(QDialog): +class GuiManuscript(NToolDialog): """GUI Tools: Manuscript Tool The dialog displays all the users build definitions, a preview panel @@ -73,15 +69,11 @@ class GuiManuscript(QDialog): D_KEY = QtUserRole - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiManuscript") self.setObjectName("GuiManuscript") - if CONFIG.osDarwin: - self.setWindowFlag(Qt.WindowType.Tool) - - self.mainGui = mainGui self._builds = BuildCollection(SHARED.project) self._buildMap: dict[str, QListWidgetItem] = {} @@ -280,7 +272,7 @@ class GuiManuscript(QDialog): dialog open. """ self._saveSettings() - for obj in self.mainGui.children(): + for obj in SHARED.mainGui.children(): # Make sure we don't have any settings windows open if isinstance(obj, GuiBuildSettings) and obj.isVisible(): obj.close() @@ -319,6 +311,8 @@ class GuiManuscript(QDialog): """Delete the currently selected build settings entry.""" if build := self._getSelectedBuild(): if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))): + if dialog := self._findSettingsDialog(build.buildID): + dialog.close() self._builds.removeBuild(build.buildID) self._updateBuildsList() return @@ -467,22 +461,14 @@ class GuiManuscript(QDialog): def _openSettingsDialog(self, build: BuildSettings) -> None: """Open the build settings dialog.""" - for obj in self.mainGui.children(): - # Don't open a second dialog if one exists - if isinstance(obj, GuiBuildSettings): - if obj.buildID == build.buildID: - logger.debug("Found instance of GuiBuildSettings") - obj.show() - obj.raise_() - return + if dialog := self._findSettingsDialog(build.buildID): + dialog.activateDialog() + return - dlgSettings = GuiBuildSettings(self.mainGui, build) - dlgSettings.setModal(False) - dlgSettings.show() - dlgSettings.raise_() - QApplication.processEvents() - dlgSettings.loadContent() - dlgSettings.newSettingsReady.connect(self._processNewSettings) + dialog = GuiBuildSettings(SHARED.mainGui, build) + dialog.activateDialog() + dialog.loadContent() + dialog.newSettingsReady.connect(self._processNewSettings) return @@ -507,6 +493,15 @@ class GuiManuscript(QDialog): self._updateBuildsList() return + def _findSettingsDialog(self, buildID: str) -> GuiBuildSettings | None: + """Return an open build settings dialog for a given build, if + one exists. + """ + for obj in SHARED.mainGui.children(): + if isinstance(obj, GuiBuildSettings) and obj.buildID == buildID: + return obj + return None + # END Class GuiManuscript diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index a27c011f..e138b592 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -25,13 +25,11 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING - -from PyQt5.QtCore import QEvent, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QEvent, pyqtSignal, pyqtSlot from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument from PyQt5.QtWidgets import ( - QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFontDialog, - QFrame, QGridLayout, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMenu, + QAbstractButton, QAbstractItemView, QDialogButtonBox, QFontDialog, QFrame, + QGridLayout, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMenu, QPlainTextEdit, QPushButton, QSplitter, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) @@ -42,7 +40,9 @@ from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.configlayout import ( NColourLabel, NFixedPage, NScrollableForm, NScrollablePage ) -from novelwriter.extensions.modified import NComboBox, NDoubleSpinBox, NIconToolButton, NSpinBox +from novelwriter.extensions.modified import ( + NComboBox, NDoubleSpinBox, NIconToolButton, NSpinBox, NToolDialog +) from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switchbox import NSwitchBox @@ -51,13 +51,10 @@ from novelwriter.types import ( QtRoleApply, QtRoleReject, QtUserRole ) -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) -class GuiBuildSettings(QDialog): +class GuiBuildSettings(NToolDialog): """GUI Tools: Manuscript Build Settings Dialog The main tool for configuring manuscript builds. It's a GUI tool for @@ -72,13 +69,11 @@ class GuiBuildSettings(QDialog): newSettingsReady = pyqtSignal(BuildSettings) - def __init__(self, mainGui: GuiMain, build: BuildSettings) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiBuildSettings") self.setObjectName("GuiBuildSettings") - if CONFIG.osDarwin: - self.setWindowFlag(Qt.WindowType.Tool) self._build = build @@ -296,8 +291,8 @@ class _FilterTab(NFixedPage): F_INCLUDED = 2 F_EXCLUDED = 3 - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: - super().__init__(parent=buildMain) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) self._treeMap: dict[str, QTreeWidgetItem] = {} self._build = build @@ -586,8 +581,8 @@ class _HeadingsTab(NScrollablePage): EDIT_HSCENE = 5 EDIT_SECTION = 6 - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: - super().__init__(parent=buildMain) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) self._build = build self._editing = 0 @@ -972,8 +967,8 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): class _ContentTab(NScrollableForm): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: - super().__init__(parent=buildMain) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) self._build = build @@ -1057,8 +1052,8 @@ class _ContentTab(NScrollableForm): class _FormatTab(NScrollableForm): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: - super().__init__(parent=buildMain) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) self._build = build self._unitScale = 1.0 @@ -1324,8 +1319,8 @@ class _FormatTab(NScrollableForm): class _OutputTab(NScrollableForm): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: - super().__init__(parent=buildMain) + def __init__(self, parent: QWidget, build: BuildSettings) -> None: + super().__init__(parent=parent) self._build = build diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 08755425..91d95651 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -23,21 +23,22 @@ along with this program. If not, see . """ from __future__ import annotations -import math import logging +import math from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout, - QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QDialogButtonBox, QFormLayout, QGridLayout, QHBoxLayout, + QLabel, QSpinBox, QStackedWidget, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime, numberToRoman from novelwriter.constants import nwUnicode from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollablePage +from novelwriter.extensions.modified import NNonBlockingDialog from novelwriter.extensions.novelselector import NovelSelector from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch @@ -46,7 +47,7 @@ from novelwriter.types import QtAlignRight, QtDecoration, QtDialogClose logger = logging.getLogger(__name__) -class GuiNovelDetails(QDialog): +class GuiNovelDetails(NNonBlockingDialog): PAGE_OVERVIEW = 1 PAGE_CONTENTS = 2 diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 1a12e40f..ad0a5508 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -32,15 +32,16 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtGui import QCloseEvent, QCursor, QPixmap from PyQt5.QtWidgets import ( - QAction, QApplication, QDialog, QDialogButtonBox, QFileDialog, QGridLayout, + QAction, QApplication, QDialogButtonBox, QFileDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QMenu, QSpinBox, QTreeWidget, QTreeWidgetItem ) from novelwriter import CONFIG, SHARED -from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax +from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax from novelwriter.constants import nwConst from novelwriter.error import formatException +from novelwriter.extensions.modified import NToolDialog from novelwriter.extensions.switch import NSwitch from novelwriter.types import ( QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration, @@ -53,7 +54,7 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) -class GuiWritingStats(QDialog): +class GuiWritingStats(NToolDialog): """GUI Tools: Writing Statistics Displays data from the NWSessionLog object. @@ -73,8 +74,6 @@ class GuiWritingStats(QDialog): logger.debug("Create: GuiWritingStats") self.setObjectName("GuiWritingStats") - if CONFIG.osDarwin: - self.setWindowFlag(Qt.WindowType.Tool) self.logData = [] self.filterData = [] diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 37feacec..40d46cee 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 5e80767f..ec88349a 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -20,18 +20,18 @@ along with this program. If not, see . """ from __future__ import annotations -import pytest import sys from pathlib import Path -from pytestqt.qtbot import QtBot + +import pytest from mocked import causeOSError -from tools import C, buildTestProject - from PyQt5.QtCore import pyqtSlot from PyQt5.QtPrintSupport import QPrintPreviewDialog from PyQt5.QtWidgets import QAction, QListWidgetItem +from pytestqt.qtbot import QtBot +from tools import C, buildTestProject from novelwriter import CONFIG, SHARED from novelwriter.constants import nwHeadFmt @@ -98,7 +98,14 @@ def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path): manus.show() manus.loadContent() - # Delete the default build + # Delete the default build, while it is open + manus.buildList.clearSelection() + manus.buildList.setCurrentRow(0) + with qtbot.waitSignal(manus.tbEdit.clicked, timeout=5000): + manus.tbEdit.click() + + manus._editSelectedBuild() + manus.buildList.clearSelection() manus.buildList.setCurrentRow(0) manus.tbDel.click() @@ -127,8 +134,10 @@ def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path): # Edit the new build manus.buildList.clearSelection() manus.buildList.setCurrentRow(0) - manus.tbEdit.click() + with qtbot.waitSignal(manus.tbEdit.clicked, timeout=5000): + manus.tbEdit.click() + manus._editSelectedBuild() bSettings = SHARED.findTopLevelWidget(GuiBuildSettings) assert isinstance(bSettings, GuiBuildSettings) build = None @@ -147,7 +156,6 @@ def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path): # May have been garbage collected, but if it isn't, it should be hidden assert bSettings.isVisible() is False - # Finish # qtbot.stop() # END Test testManuscript_Builds