From 8959af1f30d9e92a3147ba0ac79de78e921e74fa Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:01:44 +0200 Subject: [PATCH 01/10] Move make alert to separate module and add a global singleton for pointers --- novelwriter/__init__.py | 13 ++++--- novelwriter/alert.py | 78 ++++++++++++++++++++++++++++++++++++++++ novelwriter/config.py | 68 +++++++++++++++++++++++++++++++++-- novelwriter/constants.py | 9 +++-- novelwriter/enum.py | 1 - novelwriter/guimain.py | 53 ++++++--------------------- 6 files changed, 169 insertions(+), 53 deletions(-) create mode 100644 novelwriter/alert.py diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 4731b73b..1114c47f 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -21,6 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import getopt @@ -29,7 +30,7 @@ import logging from PyQt5.QtWidgets import QApplication, QErrorMessage from novelwriter.error import exceptionHandler, logException -from novelwriter.config import Config +from novelwriter.config import Config, Global ## # Version Scheme @@ -71,13 +72,13 @@ logger = logging.getLogger(__name__) # Main Program ## -# Load the main config as a global object +# Create the global singleton instances CONFIG = Config() +GLOBAL = Global() -def main(sysArgs=None): - """Parse command line, set up logging, and launch main GUI. - """ +def main(sysArgs: list | None = None): + """Parse command line, set up logging, and launch main GUI.""" if sysArgs is None: sysArgs = sys.argv[1:] @@ -227,6 +228,7 @@ def main(sysArgs=None): from novelwriter.guimain import GuiMain if testMode: nwGUI = GuiMain() + GLOBAL.setGUI(nwGUI) return nwGUI else: @@ -246,6 +248,7 @@ def main(sysArgs=None): # Launch main GUI nwGUI = GuiMain() + GLOBAL.setGUI(nwGUI) nwGUI.postLaunchTasks(cmdOpen) sys.exit(nwApp.exec_()) diff --git a/novelwriter/alert.py b/novelwriter/alert.py new file mode 100644 index 00000000..eb84107d --- /dev/null +++ b/novelwriter/alert.py @@ -0,0 +1,78 @@ +""" +novelWriter – Alert Functions +============================= + +File History: +Created: 2023-08-07 [2.1b2] + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import logging + +from PyQt5.QtWidgets import QMessageBox + +from novelwriter import GLOBAL +from novelwriter.enum import nwAlert +from novelwriter.constants import nwLabels + +logger = logging.getLogger(__name__) + + +def makeAlert(message: list[str] | str, level: nwAlert, exc: Exception | None = None) -> None: + """Alert both the user and the logger at the same time. The + message can be either a string or a list of strings. + """ + if isinstance(message, list): + message = list(filter(None, message)) # Strip empty strings + popMsg = "
".join(message) + logMsg = " ".join(message) + else: + popMsg = str(message) + logMsg = str(message) + + kw = {} + if exc is not None: + kw["exc_info"] = exc + popMsg = f"{popMsg}
{type(exc).__name__}: {str(exc)}" + + # Write to Log + if level == nwAlert.INFO: + logger.info(logMsg, **kw) + elif level == nwAlert.WARN: + logger.warning(logMsg, **kw) + elif level == nwAlert.ERROR: + logger.error(logMsg, **kw) + + # Popup + msgBox = QMessageBox() + if level == nwAlert.INFO: + msgBox.information(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) + elif level == nwAlert.WARN: + msgBox.warning(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) + elif level == nwAlert.ERROR: + msgBox.critical(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) + + return + + +def askQuestion(title: str, question: str) -> bool: + """Ask the user a Yes/No question, and return the answer.""" + msgBox = QMessageBox() + msgRes = msgBox.question(GLOBAL.gui, title, question, QMessageBox.Yes | QMessageBox.No) + return msgRes == QMessageBox.Yes diff --git a/novelwriter/config.py b/novelwriter/config.py index 60e750a9..d1fb2c88 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -1,10 +1,10 @@ """ novelWriter – Config Class ========================== -Class holding the user preferences and handling the config file File History: -Created: 2018-09-22 [0.0.1] +Created: 2018-09-22 [0.0.1] Config +Created: 2023-08-07 [2.1b2] NWGlobal This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -22,7 +22,6 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ - from __future__ import annotations import sys @@ -30,6 +29,7 @@ import json import logging from time import time +from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtGui import QFontDatabase @@ -42,6 +42,11 @@ from novelwriter.error import logException, formatException from novelwriter.common import checkPath, formatTimeStamp, NWConfigParser from novelwriter.constants import nwFiles, nwUnicode +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + from novelwriter.gui.theme import GuiTheme + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) @@ -793,6 +798,63 @@ class Config: # END Class Config +class Global: + """Singleton: Global Pointers + + This class holds pointers to the core singletons: + * The GuiMain instance + * The GuiTheme instance + * The NWProject instance + """ + + def __init__(self) -> None: + self._gui: GuiMain | None = None + self._theme: GuiTheme | None = None + self._project: NWProject | None = None + return + + @property + def gui(self) -> GuiMain: + """The main gui instance.""" + if self._gui is None: + raise Exception("GLOBAL not fully initialised") + return self._gui + + @property + def theme(self) -> GuiTheme: + """The main gui theme instance.""" + if self._theme is None: + raise Exception("GLOBAL not fully initialised") + return self._theme + + @property + def project(self) -> NWProject: + """The main project instance.""" + if self._project is None: + raise Exception("GLOBAL not fully initialised") + return self._project + + def setGUI(self, gui: GuiMain) -> None: + """Set the GUI instance. Can only be set once.""" + if self._gui is None: + self._gui = gui + return + + def setTheme(self, theme: GuiTheme) -> None: + """Set the theme instance. Can only be set once.""" + if self._theme is None: + self._theme = theme + return + + def setProject(self, project: NWProject) -> None: + """Set the project instance. Can only be set once.""" + if self._project is None: + self._project = project + return + +# END Class Global + + class RecentProjects: def __init__(self, config): diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 5a306a73..cb77570f 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -1,7 +1,6 @@ """ novelWriter – Constants ======================= -Constants and maps for translating flags and enums to text File History: Created: 2019-04-28 [0.0.1] @@ -24,9 +23,10 @@ along with this program. If not, see . """ from __future__ import annotations + from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP -from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline +from novelwriter.enum import nwAlert, nwBuildFmt, nwItemClass, nwItemLayout, nwOutline def trConst(text: str) -> str: @@ -163,6 +163,11 @@ class nwLabels: nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"), nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"), } + ALERT_NAME = { + nwAlert.INFO: QT_TRANSLATE_NOOP("Constant", "Information"), + nwAlert.WARN: QT_TRANSLATE_NOOP("Constant", "Warning"), + nwAlert.ERROR: QT_TRANSLATE_NOOP("Constant", "Error"), + } ITEM_DESCRIPTION = { "none": QT_TRANSLATE_NOOP("Constant", "None"), "root": QT_TRANSLATE_NOOP("Constant", "Root Folder"), diff --git a/novelwriter/enum.py b/novelwriter/enum.py index d96e4b2f..ab91b67e 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -125,7 +125,6 @@ class nwAlert(Enum): INFO = 0 WARN = 1 ERROR = 2 - BUG = 3 # END Enum nwAlert diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c43789ef..82814d2f 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -37,7 +37,8 @@ from PyQt5.QtWidgets import ( QStackedWidget, QVBoxLayout, QWidget ) -from novelwriter import CONFIG, __hexversion__ +from novelwriter import CONFIG, GLOBAL, __hexversion__ +from novelwriter.alert import askQuestion, makeAlert from novelwriter.gui.theme import GuiTheme from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.outline import GuiOutlineView @@ -112,9 +113,13 @@ class GuiMain(QMainWindow): # Core Classes # ============ - # Core Classes and Settings - self.mainTheme = GuiTheme() - self.theProject = NWProject(self) + # Core Classes + self.mainTheme = GuiTheme() + self.theProject = NWProject(self) + GLOBAL.setTheme(self.mainTheme) + GLOBAL.setProject(self.theProject) + + # Core Settings self.hasProject = False self.isFocusMode = False self.idleRefTime = time() @@ -1102,48 +1107,12 @@ class GuiMain(QMainWindow): """Alert both the user and the logger at the same time. The message can be either a string or a list of strings. """ - if isinstance(message, list): - message = list(filter(None, message)) # Strip empty strings - popMsg = "
".join(message) - logMsg = " ".join(message) - else: - popMsg = str(message) - logMsg = str(message) - - kw = {} - if exception is not None: - kw["exc_info"] = exception - popMsg = f"{popMsg}
{type(exception).__name__}: {str(exception)}" - - # Write to Log - if level == nwAlert.INFO: - logger.info(logMsg, **kw) - elif level == nwAlert.WARN: - logger.warning(logMsg, **kw) - elif level == nwAlert.ERROR: - logger.error(logMsg, **kw) - elif level == nwAlert.BUG: - logger.error(logMsg, **kw) - - # Popup - msgBox = QMessageBox() - if level == nwAlert.INFO: - msgBox.information(self, self.tr("Information"), popMsg) - elif level == nwAlert.WARN: - msgBox.warning(self, self.tr("Warning"), popMsg) - elif level == nwAlert.ERROR: - msgBox.critical(self, self.tr("Error"), popMsg) - elif level == nwAlert.BUG: - popMsg += "
%s" % self.tr("This is a bug!") - msgBox.critical(self, self.tr("Internal Error"), popMsg) - + makeAlert(message, level, exception) return def askQuestion(self, title: str, question: str) -> bool: """Ask the user a Yes/No question, and return the answer.""" - msgBox = QMessageBox() - msgRes = msgBox.question(self, title, question, QMessageBox.Yes | QMessageBox.No) - return msgRes == QMessageBox.Yes + return askQuestion(title, question) def reportConfErr(self) -> bool: """Checks if the Config module has any errors to report, and let From ec010bf18e3361c596aed4dbc01572c2e94cd4bf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:02:18 +0200 Subject: [PATCH 02/10] Add new icons for alert boxes --- .../assets/icons/typicons_dark/icons.conf | 4 ++++ .../icons/typicons_dark/typ_delete-full.svg | 4 ++++ .../icons/typicons_dark/typ_info-large-full.svg | 4 ++++ .../icons/typicons_dark/typ_lightbulb-full.svg | 4 ++++ .../icons/typicons_dark/typ_warning-full.svg | 4 ++++ .../assets/icons/typicons_light/icons.conf | 4 ++++ .../icons/typicons_light/typ_delete-full.svg | 4 ++++ .../typicons_light/typ_info-large-full.svg | 4 ++++ .../icons/typicons_light/typ_lightbulb-full.svg | 4 ++++ .../icons/typicons_light/typ_warning-full.svg | 4 ++++ novelwriter/gui/theme.py | 17 +++++++++-------- 11 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_delete-full.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_warning-full.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_delete-full.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_info-large-full.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_warning-full.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index 8e354654..d209978a 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -17,6 +17,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg +alert_error = typ_info-large-full.svg +alert_info = typ_delete-full.svg +alert_question = typ_lightbulb-full.svg +alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg bookmark = typ_bookmark.svg browse = typ_folder-open.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_delete-full.svg b/novelwriter/assets/icons/typicons_dark/typ_delete-full.svg new file mode 100644 index 00000000..2dbe7364 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_delete-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg b/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg new file mode 100644 index 00000000..c9a13390 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg b/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg new file mode 100644 index 00000000..7397b602 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg b/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg new file mode 100644 index 00000000..e6022e06 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index a52469ea..37946169 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -17,6 +17,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg +alert_error = typ_info-large-full.svg +alert_info = typ_delete-full.svg +alert_question = typ_lightbulb-full.svg +alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg bookmark = typ_bookmark.svg browse = typ_folder-open.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_delete-full.svg b/novelwriter/assets/icons/typicons_light/typ_delete-full.svg new file mode 100644 index 00000000..5a4dd8cd --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_delete-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg b/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg new file mode 100644 index 00000000..80a43508 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg b/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg new file mode 100644 index 00000000..daf4254c --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_warning-full.svg b/novelwriter/assets/icons/typicons_light/typ_warning-full.svg new file mode 100644 index 00000000..d6a3b52c --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_warning-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index f951fc79..c65fad5d 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -445,14 +445,15 @@ class GuiIcons: ICON_KEYS = { # Project and GUI Icons - "novelwriter", "build_excluded", "build_filtered", "build_included", "cls_archive", - "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", - "cls_plot", "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", - "proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", - "proj_stats", "proj_title", "search_cancel", "search_case", "search_loop", - "search_preserve", "search_project", "search_regex", "search_word", "status_idle", - "status_lang", "status_lines", "status_stats", "status_time", "view_build", "view_editor", - "view_novel", "view_outline", + "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn", + "build_excluded", "build_filtered", "build_included", "cls_archive", "cls_character", + "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", "cls_plot", + "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", "proj_document", + "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", "proj_stats", + "proj_title", "search_cancel", "search_case", "search_loop", "search_preserve", + "search_project", "search_regex", "search_word", "status_idle", "status_lang", + "status_lines", "status_stats", "status_time", "view_build", "view_editor", "view_novel", + "view_outline", # General Button Icons "add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit", From 90f9fa44510f71f05be16d491f74313abafd27c2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:19:51 +0200 Subject: [PATCH 03/10] Rewrite the makeAlert and askQuestion functions to use recommended interface --- novelwriter/__init__.py | 8 +- novelwriter/alert.py | 78 -------- .../assets/icons/typicons_dark/icons.conf | 4 +- .../assets/icons/typicons_light/icons.conf | 4 +- novelwriter/config.py | 4 +- novelwriter/constants.py | 1 + novelwriter/enum.py | 1 + novelwriter/error.py | 38 ++-- novelwriter/guimain.py | 167 ++++++++++-------- 9 files changed, 123 insertions(+), 182 deletions(-) delete mode 100644 novelwriter/alert.py diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 1114c47f..d83415f7 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -30,7 +30,7 @@ import logging from PyQt5.QtWidgets import QApplication, QErrorMessage from novelwriter.error import exceptionHandler, logException -from novelwriter.config import Config, Global +from novelwriter.config import Config, NWApp ## # Version Scheme @@ -74,7 +74,7 @@ logger = logging.getLogger(__name__) # Create the global singleton instances CONFIG = Config() -GLOBAL = Global() +APP = NWApp() def main(sysArgs: list | None = None): @@ -228,7 +228,7 @@ def main(sysArgs: list | None = None): from novelwriter.guimain import GuiMain if testMode: nwGUI = GuiMain() - GLOBAL.setGUI(nwGUI) + APP.setGUI(nwGUI) return nwGUI else: @@ -248,7 +248,7 @@ def main(sysArgs: list | None = None): # Launch main GUI nwGUI = GuiMain() - GLOBAL.setGUI(nwGUI) + APP.setGUI(nwGUI) nwGUI.postLaunchTasks(cmdOpen) sys.exit(nwApp.exec_()) diff --git a/novelwriter/alert.py b/novelwriter/alert.py deleted file mode 100644 index eb84107d..00000000 --- a/novelwriter/alert.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -novelWriter – Alert Functions -============================= - -File History: -Created: 2023-08-07 [2.1b2] - -This file is a part of novelWriter -Copyright 2018–2023, Veronica Berglyd Olsen - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -from __future__ import annotations - -import logging - -from PyQt5.QtWidgets import QMessageBox - -from novelwriter import GLOBAL -from novelwriter.enum import nwAlert -from novelwriter.constants import nwLabels - -logger = logging.getLogger(__name__) - - -def makeAlert(message: list[str] | str, level: nwAlert, exc: Exception | None = None) -> None: - """Alert both the user and the logger at the same time. The - message can be either a string or a list of strings. - """ - if isinstance(message, list): - message = list(filter(None, message)) # Strip empty strings - popMsg = "
".join(message) - logMsg = " ".join(message) - else: - popMsg = str(message) - logMsg = str(message) - - kw = {} - if exc is not None: - kw["exc_info"] = exc - popMsg = f"{popMsg}
{type(exc).__name__}: {str(exc)}" - - # Write to Log - if level == nwAlert.INFO: - logger.info(logMsg, **kw) - elif level == nwAlert.WARN: - logger.warning(logMsg, **kw) - elif level == nwAlert.ERROR: - logger.error(logMsg, **kw) - - # Popup - msgBox = QMessageBox() - if level == nwAlert.INFO: - msgBox.information(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) - elif level == nwAlert.WARN: - msgBox.warning(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) - elif level == nwAlert.ERROR: - msgBox.critical(GLOBAL.gui, nwLabels.ALERT_NAME[level], popMsg) - - return - - -def askQuestion(title: str, question: str) -> bool: - """Ask the user a Yes/No question, and return the answer.""" - msgBox = QMessageBox() - msgRes = msgBox.question(GLOBAL.gui, title, question, QMessageBox.Yes | QMessageBox.No) - return msgRes == QMessageBox.Yes diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index d209978a..32a523f7 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -17,8 +17,8 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg -alert_error = typ_info-large-full.svg -alert_info = typ_delete-full.svg +alert_error = typ_delete-full.svg +alert_info = typ_info-large-full.svg alert_question = typ_lightbulb-full.svg alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 37946169..4a2ce078 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -17,8 +17,8 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg -alert_error = typ_info-large-full.svg -alert_info = typ_delete-full.svg +alert_error = typ_delete-full.svg +alert_info = typ_info-large-full.svg alert_question = typ_lightbulb-full.svg alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg diff --git a/novelwriter/config.py b/novelwriter/config.py index d1fb2c88..7bbf125e 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -798,7 +798,7 @@ class Config: # END Class Config -class Global: +class NWApp: """Singleton: Global Pointers This class holds pointers to the core singletons: @@ -852,7 +852,7 @@ class Global: self._project = project return -# END Class Global +# END Class NWApp class RecentProjects: diff --git a/novelwriter/constants.py b/novelwriter/constants.py index cb77570f..c3abf581 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -167,6 +167,7 @@ class nwLabels: nwAlert.INFO: QT_TRANSLATE_NOOP("Constant", "Information"), nwAlert.WARN: QT_TRANSLATE_NOOP("Constant", "Warning"), nwAlert.ERROR: QT_TRANSLATE_NOOP("Constant", "Error"), + nwAlert.ASK: QT_TRANSLATE_NOOP("Constant", "Question"), } ITEM_DESCRIPTION = { "none": QT_TRANSLATE_NOOP("Constant", "None"), diff --git a/novelwriter/enum.py b/novelwriter/enum.py index ab91b67e..aeab3f5e 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -125,6 +125,7 @@ class nwAlert(Enum): INFO = 0 WARN = 1 ERROR = 2 + ASK = 3 # END Enum nwAlert diff --git a/novelwriter/error.py b/novelwriter/error.py index 73c08290..8b4eeba6 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -1,7 +1,6 @@ """ novelWriter – Exception Handling ================================ -Error handling function and error dialog File History: Created: 2020-08-02 [0.10.2] @@ -22,18 +21,24 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import random import logging +from typing import TYPE_CHECKING + from PyQt5.QtGui import QFont, QFontDatabase -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel, + QWidget, qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel, QDialogButtonBox ) +if TYPE_CHECKING: # pragma: no cover + from types import TracebackType + logger = logging.getLogger(__name__) @@ -41,15 +46,15 @@ logger = logging.getLogger(__name__) # Utility Functions # =============================================================================================== # -def logException(): - """Log the content of an exception message. - """ +def logException() -> None: + """Log the content of an exception message.""" exType, exValue, _ = sys.exc_info() if exType is not None: logger.error("%s: %s", exType.__name__, str(exValue)) + return -def formatException(exc): +def formatException(exc) -> str: """Format an exception as a string the same way the default exception handler does. """ @@ -62,7 +67,7 @@ def formatException(exc): class NWErrorMessage(QDialog): - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self.setObjectName("NWErrorMessage") @@ -113,7 +118,7 @@ class NWErrorMessage(QDialog): return - def setMessage(self, exType, exValue, exTrace): + def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None: """Generate a message and append session data, error info and error traceback. """ @@ -142,7 +147,7 @@ class NWErrorMessage(QDialog): enchantVersion = "Unknown" try: - exTrace = "\n".join(format_tb(exTrace)) + txtTrace = "\n".join(format_tb(exTrace)) self.msgBody.setPlainText(( "Environment:\n" f"novelWriter Version: {__version__}\n" @@ -151,7 +156,7 @@ class NWErrorMessage(QDialog): f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"enchant: {enchantVersion}\n\n" f"{exType.__name__}:\n{str(exValue)}\n\n" - f"Traceback:\n{exTrace}\n" + f"Traceback:\n{txtTrace}\n" )) except Exception: self.msgBody.setPlainText("Failed to generate error report ...") @@ -162,18 +167,17 @@ class NWErrorMessage(QDialog): # Slots ## - def _doClose(self): - """Close the dialog. - """ + @pyqtSlot() + def _doClose(self) -> None: + """Close the dialog.""" self.close() return # END Class NWErrorMessage -def exceptionHandler(exType, exValue, exTrace): - """Function to catch unhandled global exceptions. - """ +def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None: + """Function to catch unhandled global exceptions.""" from traceback import print_tb from PyQt5.QtWidgets import qApp diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 82814d2f..67e674e1 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -31,14 +31,13 @@ from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence +from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence, QPixmap from PyQt5.QtWidgets import ( qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget ) -from novelwriter import CONFIG, GLOBAL, __hexversion__ -from novelwriter.alert import askQuestion, makeAlert +from novelwriter import CONFIG, APP, __hexversion__ from novelwriter.gui.theme import GuiTheme from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.outline import GuiOutlineView @@ -67,7 +66,7 @@ from novelwriter.enum import ( nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView ) from novelwriter.common import getGuiItem, hexToInt -from novelwriter.constants import nwFiles +from novelwriter.constants import nwFiles, nwLabels, trConst logger = logging.getLogger(__name__) @@ -116,8 +115,8 @@ class GuiMain(QMainWindow): # Core Classes self.mainTheme = GuiTheme() self.theProject = NWProject(self) - GLOBAL.setTheme(self.mainTheme) - GLOBAL.setProject(self.theProject) + APP.setTheme(self.mainTheme) + APP.setProject(self.theProject) # Core Settings self.hasProject = False @@ -318,6 +317,16 @@ class GuiMain(QMainWindow): # Handle Windows Mode self.showNormal() + # Cache Icons + + aPx = CONFIG.pxInt(48) + self.alertPix: dict[nwAlert, QPixmap] = { + nwAlert.INFO: self.mainTheme.getPixmap("alert_info", (aPx, aPx)), + nwAlert.WARN: self.mainTheme.getPixmap("alert_warn", (aPx, aPx)), + nwAlert.ERROR: self.mainTheme.getPixmap("alert_error", (aPx, aPx)), + nwAlert.ASK: self.mainTheme.getPixmap("alert_question", (aPx, aPx)), + } + logger.debug("Ready: GUI") if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: @@ -325,7 +334,7 @@ class GuiMain(QMainWindow): "You are running an untested development version of novelWriter. " "Please be careful when working on a live project " "and make sure you take regular backups." - ), nwAlert.WARN) + ), level=nwAlert.WARN) logger.info("novelWriter is ready ...") self.setStatus(self.tr("novelWriter is ready ...")) @@ -385,7 +394,7 @@ class GuiMain(QMainWindow): if not self.closeProject(): self.makeAlert(self.tr( "Cannot create a new project when another project is open." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False if projData is None: @@ -403,7 +412,7 @@ class GuiMain(QMainWindow): self.makeAlert(self.tr( "A project already exists in that location. " "Please choose another folder." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False logger.info("Creating new project") @@ -425,13 +434,10 @@ class GuiMain(QMainWindow): return True if not isYes: - msgYes = self.askQuestion( - self.tr("Close Project"), - "%s
%s" % ( - self.tr("Close the current project?"), - self.tr("Changes are saved automatically.") - ) - ) + msgYes = self.askQuestion("%s
%s" % ( + self.tr("Close the current project?"), + self.tr("Changes are saved automatically.") + )) if not msgYes: return False @@ -443,10 +449,7 @@ class GuiMain(QMainWindow): if self.theProject.data.doBackup and CONFIG.backupOnClose: doBackup = True if CONFIG.askBeforeBackup: - msgYes = self.askQuestion( - self.tr("Backup Project"), - self.tr("Backup the current project?") - ) + msgYes = self.askQuestion(self.tr("Backup the current project?")) if not msgYes: doBackup = False @@ -485,19 +488,28 @@ class GuiMain(QMainWindow): # Try to open the project if not self.theProject.openProject(projFile): # The project open failed. - lockStatus = self.theProject.getLockStatus() if lockStatus is None: # The project is not locked, so failed for some other # reason handled by the project class. return False + lockText = self.tr( + "The project is already open by another instance of " + "novelWriter, and is therefore locked. Override lock " + "and continue anyway?" + ) + lockInfo = self.tr( + "Note: If the program or the computer previously " + "crashed, the lock can safely be overridden. However, " + "overriding it is not recommended if the project is " + "open in another instance of novelWriter. Doing so " + "may corrupt the project." + ) try: - lockDetails = ( - "
%s" % self.tr( - "The project was locked by the computer " - "'{0}' ({1} {2}), last active on {3}." - ) + lockDetails = self.tr( + "The project was locked by the computer " + "'{0}' ({1} {2}), last active on {3}." ).format( lockStatus[0], lockStatus[1], lockStatus[2], datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X") @@ -505,27 +517,7 @@ class GuiMain(QMainWindow): except Exception: lockDetails = "" - msgBox = QMessageBox() - msgRes = msgBox.warning( - self, self.tr("Project Locked"), - "%s

%s
%s" % ( - self.tr( - "The project is already open by another instance of " - "novelWriter, and is therefore locked. Override lock " - "and continue anyway?" - ), - self.tr( - "Note: If the program or the computer previously " - "crashed, the lock can safely be overridden. However, " - "overriding it is not recommended if the project is " - "open in another instance of novelWriter. Doing so " - "may corrupt the project." - ), - lockDetails - ), - QMessageBox.Yes | QMessageBox.No, QMessageBox.No - ) - if msgRes == QMessageBox.Yes: + if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN): if not self.theProject.openProject(projFile, overrideLock=True): return False else: @@ -565,9 +557,7 @@ class GuiMain(QMainWindow): # Check if we need to rebuild the index if self.theProject.index.indexBroken: - self.makeAlert(self.tr( - "The project index is outdated or broken. Rebuilding index." - ), nwAlert.INFO) + self.makeAlert(self.tr("The project index is outdated or broken. Rebuilding index.")) self.rebuildIndex() # Make sure the changed status is set to false on things opened @@ -756,23 +746,20 @@ class GuiMain(QMainWindow): except Exception as exc: self.makeAlert(self.tr( "Could not read file. The file must be an existing text file." - ), nwAlert.ERROR, exception=exc) + ), level=nwAlert.ERROR, exception=exc) return False if self.docEditor.docHandle() is None: self.makeAlert(self.tr( "Please open a document to import the text file into." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False if not self.docEditor.isEmpty(): - msgYes = self.askQuestion( - self.tr("Import Document"), - self.tr( - "Importing the file will overwrite the current content of " - "the document. Do you want to proceed?" - ) - ) + msgYes = self.askQuestion(self.tr( + "Importing the file will overwrite the current content of " + "the document. Do you want to proceed?" + )) if not msgYes: return False @@ -871,9 +858,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() if not beQuiet: - self.makeAlert(self.tr( - "The project index has been successfully rebuilt." - ), nwAlert.INFO) + self.makeAlert(self.tr("The project index has been successfully rebuilt.")) return True @@ -921,7 +906,7 @@ class GuiMain(QMainWindow): if dlgConf.needsRestart: self.makeAlert(self.tr( "Some changes will not be applied until novelWriter has been restarted." - ), nwAlert.INFO) + )) if dlgConf.refreshTree: self.projView.populateTree() @@ -1102,17 +1087,48 @@ class GuiMain(QMainWindow): return - def makeAlert(self, message: list[str] | str, level: nwAlert = nwAlert.INFO, - exception: Exception | None = None) -> None: + def makeAlert(self, text: str, info: str = "", details: str = "", + level: nwAlert = nwAlert.INFO, exception: Exception | None = None) -> None: """Alert both the user and the logger at the same time. The message can be either a string or a list of strings. """ - makeAlert(message, level, exception) + logText = " ".join(filter(None, [text, info, details])) + if level == nwAlert.INFO: + logger.info(logText, stacklevel=2) + elif level == nwAlert.WARN: + logger.warning(logText, stacklevel=2) + elif level == nwAlert.ERROR: + logger.error(logText, stacklevel=2, exc_info=exception) + + if exception is not None: + excText = f"{type(exception).__name__}: {str(exception)}" + info = f"{info}
{excText}" if info else excText + + msgBox = QMessageBox(self) + msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) + msgBox.setText(text) + msgBox.setInformativeText(info) + msgBox.setDetailedText(details) + msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msgBox.setIconPixmap(self.alertPix[level]) + msgBox.adjustSize() + msgBox.exec_() + return - def askQuestion(self, title: str, question: str) -> bool: + def askQuestion(self, text: str, info: str = "", details: str = "", + level: nwAlert = nwAlert.ASK) -> bool: """Ask the user a Yes/No question, and return the answer.""" - return askQuestion(title, question) + msgBox = QMessageBox(self) + msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) + msgBox.setText(text) + msgBox.setInformativeText(info) + msgBox.setDetailedText(details) + msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msgBox.setIconPixmap(self.alertPix[level]) + msgBox.adjustSize() + msgBox.exec_() + return msgBox.result() == QMessageBox.Yes def reportConfErr(self) -> bool: """Checks if the Config module has any errors to report, and let @@ -1120,7 +1136,7 @@ class GuiMain(QMainWindow): errors since it is initialised before the GUI itself. """ if CONFIG.hasError: - self.makeAlert(CONFIG.errorText(), nwAlert.ERROR) + self.makeAlert(CONFIG.errorText(), level=nwAlert.ERROR) return True return False @@ -1131,13 +1147,10 @@ class GuiMain(QMainWindow): def closeMain(self) -> bool: """Save everything, and close novelWriter.""" if self.hasProject: - msgYes = self.askQuestion( - self.tr("Exit"), - "%s
%s" % ( - self.tr("Do you want to exit novelWriter?"), - self.tr("Changes are saved automatically.") - ) - ) + msgYes = self.askQuestion("%s
%s" % ( + self.tr("Do you want to exit novelWriter?"), + self.tr("Changes are saved automatically.") + )) if not msgYes: return False @@ -1388,7 +1401,7 @@ class GuiMain(QMainWindow): "from the Tools menu, or by pressing {1}." ).format( tag, "F9" - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return None, None return tHandle, sTitle From 3911bc10c6392e245a9830d377d01c634251635f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:20:04 +0200 Subject: [PATCH 04/10] Clean up code and tests --- novelwriter/core/coretools.py | 4 +- novelwriter/core/project.py | 61 +++++++++++------------ novelwriter/dialogs/projload.py | 11 ++-- novelwriter/dialogs/projsettings.py | 11 ++-- novelwriter/dialogs/wordlist.py | 4 +- novelwriter/gui/doceditor.py | 55 ++++++++++---------- novelwriter/gui/projtree.py | 54 +++++++++----------- novelwriter/tools/manusbuild.py | 3 +- novelwriter/tools/manuscript.py | 6 +-- novelwriter/tools/manussettings.py | 6 +-- novelwriter/tools/writingstats.py | 14 +++--- tests/conftest.py | 6 +-- tests/mocked.py | 16 +++--- tests/test_core/test_core_project.py | 6 +-- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_i18n.py | 9 ++-- tests/test_gui/test_gui_mainmenu.py | 10 ++-- tests/test_gui/test_gui_projtree.py | 10 ++-- tests/test_tools/test_tools_manusbuild.py | 2 +- 19 files changed, 131 insertions(+), 159 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index e7791613..114c92ff 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -479,7 +479,7 @@ class ProjectBuilder: except Exception as exc: self.mainGui.makeAlert(self.tr( "Failed to create a new example project." - ), nwAlert.ERROR, exception=exc) + ), level=nwAlert.ERROR, exception=exc) return False else: @@ -487,7 +487,7 @@ class ProjectBuilder: "Failed to create a new example project. " "Could not find the necessary files. " "They seem to be missing from this installation." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False return True diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 20fb6dc2..88450946 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -173,9 +173,10 @@ class NWProject(QObject): if self._tree.checkType(tHandle, nwItemType.FILE): delDoc = self._storage.getDocument(tHandle) if not delDoc.deleteDocument(): - self.mainGui.makeAlert([ - self.tr("Could not delete document file."), delDoc.getError() - ], nwAlert.ERROR) + self.mainGui.makeAlert( + self.tr("Could not delete document file."), + info=delDoc.getError(), level=nwAlert.ERROR + ) return False self._index.deleteHandle(tHandle) @@ -227,7 +228,7 @@ class NWProject(QObject): if not self._storage.openProjectInPlace(projPath): self.mainGui.makeAlert(self.tr( "Could not open project with path: {0}" - ).format(projPath), nwAlert.ERROR) + ).format(projPath), level=nwAlert.ERROR) return False # Project Lock @@ -266,17 +267,17 @@ class NWProject(QObject): if xmlReader.state == XMLReadState.NOT_NWX_FILE: self.mainGui.makeAlert(self.tr( "Project file does not appear to be a novelWriterXML file." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) elif xmlReader.state == XMLReadState.UNKNOWN_VERSION: self.mainGui.makeAlert(self.tr( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " "The file was saved with novelWriter version {0}." - ).format(appVersion), nwAlert.ERROR) + ).format(appVersion), level=nwAlert.ERROR) else: self.mainGui.makeAlert(self.tr( "Failed to parse project xml." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) self.clearProject() return False @@ -285,14 +286,11 @@ class NWProject(QObject): # ==================== if xmlReader.state == XMLReadState.WAS_LEGACY: - msgYes = self.mainGui.askQuestion( - self.tr("File Version"), - self.tr( - "The file format of your project is about to be updated. " - "If you proceed, older versions of novelWriter will no " - "longer be able to open this project. Continue?" - ) - ) + msgYes = self.mainGui.askQuestion(self.tr( + "The file format of your project is about to be updated. " + "If you proceed, older versions of novelWriter will no " + "longer be able to open this project. Continue?" + )) if not msgYes: self.clearProject() return False @@ -301,16 +299,13 @@ class NWProject(QObject): # ========================= if xmlReader.hexVersion > hexToInt(__hexversion__): - msgYes = self.mainGui.askQuestion( - self.tr("Version Conflict"), - self.tr( - "This project was saved by a newer version of " - "novelWriter, version {0}. This is version {1}. If you " - "continue to open the project, some attributes and " - "settings may not be preserved, but the overall project " - "should be fine. Continue opening the project?" - ).format(appVersion, __version__) - ) + msgYes = self.mainGui.askQuestion(self.tr( + "This project was saved by a newer version of " + "novelWriter, version {0}. This is version {1}. If you " + "continue to open the project, some attributes and " + "settings may not be preserved, but the overall project " + "should be fine. Continue opening the project?" + ).format(appVersion, __version__)) if not msgYes: self.clearProject() return False @@ -333,7 +328,7 @@ class NWProject(QObject): if orphans > 0: self.mainGui.makeAlert(self.tr( "Found {0} orphaned file(s) in the project. {1} file(s) were recovered." - ).format(orphans, recovered), nwAlert.WARN) + ).format(orphans, recovered), level=nwAlert.WARN) self._index.loadIndex() if xmlReader.state == XMLReadState.WAS_LEGACY: @@ -357,7 +352,7 @@ class NWProject(QObject): if not self._storage.isOpen(): self.mainGui.makeAlert(self.tr( "There is no project open." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False saveTime = time() @@ -382,7 +377,7 @@ class NWProject(QObject): if not xmlWriter.write(self._data, content, saveTime, editTime): self.mainGui.makeAlert(self.tr( "Failed to save project." - ), nwAlert.ERROR, exception=xmlWriter.error) + ), level=nwAlert.ERROR, exception=xmlWriter.error) return False # Save other project data @@ -426,14 +421,14 @@ class NWProject(QObject): self.mainGui.makeAlert(self.tr( "Cannot backup project because no valid backup path is set. " "Please set a valid backup location in Preferences." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False if not self._data.name: self.mainGui.makeAlert(self.tr( "Cannot backup project because no project name is set. " "Please set a Project Name in Project Settings." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False cleanName = makeFileNameSafe(self._data.name) @@ -443,7 +438,7 @@ class NWProject(QObject): except Exception as exc: self.mainGui.makeAlert(self.tr( "Could not create backup folder." - ), nwAlert.ERROR, exception=exc) + ), level=nwAlert.ERROR, exception=exc) return False timeStamp = formatTimeStamp(time(), fileSafe=True) @@ -453,11 +448,11 @@ class NWProject(QObject): if doNotify: self.mainGui.makeAlert(self.tr( "Backup archive file written to: {0} [{1}B]" - ).format(str(archName), formatInt(size)), nwAlert.INFO) + ).format(str(archName), formatInt(size))) else: self.mainGui.makeAlert(self.tr( "Could not write backup archive." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False self.mainGui.setStatus(self.tr( diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index 04cef031..916ca820 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -225,13 +225,10 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: projName = selList[0].text(self.C_NAME) - msgYes = self.mainGui.askQuestion( - self.tr("Remove Entry"), - self.tr( - "Remove '{0}' from the recent projects list? " - "The project files will not be deleted." - ).format(projName) - ) + msgYes = self.mainGui.askQuestion(self.tr( + "Remove '{0}' from the recent projects list? " + "The project files will not be deleted." + ).format(projName)) if msgYes: CONFIG.recentProjects.remove( selList[0].data(self.C_NAME, self.D_PATH) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index f496b5e8..a639f42c 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -1,7 +1,6 @@ """ novelWriter – GUI Project Settings ================================== -GUI classes for the project settings dialog File History: Created: 2018-09-29 [0.0.1] @@ -22,9 +21,12 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import logging +from typing import TYPE_CHECKING + from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtCore import Qt, QLocale, pyqtSlot from PyQt5.QtWidgets import ( @@ -39,6 +41,9 @@ from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.pageddialog import NPagedDialog from novelwriter.extensions.configlayout import NConfigLayout +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -49,7 +54,7 @@ class GuiProjectSettings(NPagedDialog): TAB_IMPORT = 2 TAB_REPLACE = 3 - def __init__(self, mainGui, focusTab=TAB_MAIN): + def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiProjectSettings") @@ -441,7 +446,7 @@ class GuiProjectEditStatus(QWidget): if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: self.mainGui.makeAlert(self.tr( "Cannot delete a status item that is in use." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) else: self.listBox.takeTopLevelItem(iRow) self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 55503376..9f807162 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -128,13 +128,13 @@ class GuiWordList(QDialog): if word == "": self.mainGui.makeAlert(self.tr( "Cannot add a blank word." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return if self.listBox.findItems(word, Qt.MatchExactly): self.mainGui.makeAlert(self.tr( "The word '{0}' is already in the word list." - ).format(word), nwAlert.ERROR) + ).format(word), level=nwAlert.ERROR) return self.listBox.addItem(word) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b30357d2..9734bc45 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1,7 +1,6 @@ """ novelWriter – GUI Document Editor ================================= -GUI classes for the main document editor File History: Created: 2018-09-29 [0.0.1] GuiDocEditor @@ -28,12 +27,14 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import bisect import logging from enum import Enum from time import time +from typing import TYPE_CHECKING from PyQt5.QtCore import ( Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression, @@ -44,9 +45,8 @@ from PyQt5.QtGui import ( QPalette, QTextDocument, QCursor, QPixmap ) from PyQt5.QtWidgets import ( - qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, - QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, - QFrame + QAction, qApp, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, + QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget ) from novelwriter import CONFIG @@ -57,6 +57,9 @@ from novelwriter.core.index import countWords from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.gui.dochighlight import GuiDocHighlighter +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -75,7 +78,7 @@ class GuiDocEditor(QTextEdit): novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain): super().__init__(parent=mainGui) logger.debug("Create: GuiDocEditor") @@ -357,7 +360,7 @@ class GuiDocEditor(QTextEdit): ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) self.clearEditor() return False @@ -457,7 +460,7 @@ class GuiDocEditor(QTextEdit): ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) @@ -496,20 +499,19 @@ class GuiDocEditor(QTextEdit): if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument._currHash != self._nwDocument._prevHash: - msgYes = self.mainGui.askQuestion( - self.tr("File Changed on Disk"), - self.tr( - "This document has been changed outside of novelWriter " - "while it was open. Overwrite the file on disk?" - ) - ) + msgYes = self.mainGui.askQuestion(self.tr( + "This document has been changed outside of novelWriter " + "while it was open. Overwrite the file on disk?" + )) if msgYes: saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) if not saveOk: - self.mainGui.makeAlert([ - self.tr("Could not save document."), self._nwDocument.getError() - ], nwAlert.ERROR) + self.mainGui.makeAlert( + self.tr("Could not save document."), + info=self._nwDocument.getError(), + level=nwAlert.ERROR + ) return False @@ -726,7 +728,7 @@ class GuiDocEditor(QTextEdit): self.mainGui.makeAlert(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." - ), nwAlert.INFO) + )) theMode = False if self.spEnchant.spellLanguage is None: @@ -868,17 +870,10 @@ class GuiDocEditor(QTextEdit): if self._nwDocument is None: logger.error("No document open") return False - - msgBox = QMessageBox() - msgBox.information( - self, - self.tr("File Location"), - "%s
%s" % ( - self.tr("The currently open file is saved in:"), - self._nwDocument.getFileLocation() - ), + self.mainGui.makeAlert( + self.tr("The currently open file is saved in:"), + info=self._nwDocument.getFileLocation() ) - return def insertText(self, theInsert): @@ -1116,7 +1111,7 @@ class GuiDocEditor(QTextEdit): "The maximum size of a single novelWriter document is {0} MB." ).format( f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) self.undo() return @@ -1675,7 +1670,7 @@ class GuiDocEditor(QTextEdit): if not theCursor.hasSelection(): self.mainGui.makeAlert(self.tr( "Please select some text before calling replace quotes." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False posS = theCursor.selectionStart() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index d77797f8..de547135 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -589,7 +589,7 @@ class GuiProjectTree(QTreeWidget): if sHandle is None or pItem is None: self.mainGui.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False # Collect some information about the selected item @@ -600,7 +600,7 @@ class GuiProjectTree(QTreeWidget): if self.theProject.tree.isTrash(sHandle): self.mainGui.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False # Set default label and determine if new item is to be added @@ -838,7 +838,7 @@ class GuiProjectTree(QTreeWidget): if trashHandle is None: self.mainGui.makeAlert(self.tr( "There is currently no Trash folder in this project." - ), nwAlert.INFO) + )) return False theTrash = self.getTreeFromHandle(trashHandle) @@ -849,11 +849,10 @@ class GuiProjectTree(QTreeWidget): if nTrash == 0: self.mainGui.makeAlert(self.tr( "The Trash folder is already empty." - ), nwAlert.INFO) + )) return False msgYes = self.mainGui.askQuestion( - self.tr("Empty Trash"), self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) ) if not msgYes: @@ -900,7 +899,6 @@ class GuiProjectTree(QTreeWidget): if askFirst: msgYes = self.mainGui.askQuestion( - self.tr("Delete"), self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), ) if not msgYes: @@ -937,7 +935,7 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() > 0: self.mainGui.makeAlert(self.tr( "Root folders can only be deleted when they are empty." - ), nwAlert.ERROR) + ), level=nwAlert.ERROR) return False logger.debug("Permanently deleting root folder '%s'", tHandle) @@ -956,7 +954,6 @@ class GuiProjectTree(QTreeWidget): else: if askFirst: msgYes = self.mainGui.askQuestion( - self.tr("Delete"), self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) ) if not msgYes: @@ -1525,13 +1522,10 @@ class GuiProjectTree(QTreeWidget): """Convert a folder to a note or document.""" tItem = self.theProject.tree[tHandle] if tItem is not None and tItem.isFolderType(): - msgYes = self.mainGui.askQuestion( - self.tr("Convert Folder"), - self.tr( - "Do you want to convert the folder to a {0}? " - "This action cannot be reversed." - ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])) - ) + msgYes = self.mainGui.askQuestion(self.tr( + "Do you want to convert the folder to a {0}? " + "This action cannot be reversed." + ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))) if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): tItem.setType(nwItemType.FILE) tItem.setLayout(nwItemLayout.DOCUMENT) @@ -1570,9 +1564,7 @@ class GuiProjectTree(QTreeWidget): mrgData = dlgMerge.getData() mrgList = mrgData.get("finalItems", []) if not mrgList: - self.mainGui.makeAlert([ - self.tr("No documents selected for merging.") - ], nwAlert.INFO) + self.mainGui.makeAlert(self.tr("No documents selected for merging.")) return False # Save the open document first, in case it's part of merge @@ -1595,9 +1587,10 @@ class GuiProjectTree(QTreeWidget): docMerger.appendText(sHandle, True, mLabel) if not docMerger.writeTargetDoc(): - self.mainGui.makeAlert([ - self.tr("Could not write document content."), docMerger.getError() - ], nwAlert.ERROR) + self.mainGui.makeAlert( + self.tr("Could not write document content."), + info=docMerger.getError(), level=nwAlert.ERROR + ) return False self.theProject.index.reIndexHandle(mHandle) @@ -1658,9 +1651,10 @@ class GuiProjectTree(QTreeWidget): self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) self._alertTreeChange(dHandle, flush=False) if not writeOk: - self.mainGui.makeAlert([ - self.tr("Could not write document content."), docSplit.getError() - ], nwAlert.ERROR) + self.mainGui.makeAlert( + self.tr("Could not write document content."), + info=docSplit.getError(), level=nwAlert.ERROR + ) if splitData.get("moveToTrash", False): self.moveItemToTrash(tHandle, askFirst=False, flush=True) @@ -1680,13 +1674,11 @@ class GuiProjectTree(QTreeWidget): if nItems == 0: return False elif nItems == 1: - qTitle = self.tr("Duplicate Document") - qText = self.tr("Do you want to duplicate this document?") + question = self.tr("Do you want to duplicate this document?") else: - qTitle = self.tr("Duplicate from Here") - qText = self.tr("Do you want to duplicate this item and all child items?") + question = self.tr("Do you want to duplicate this item and all child items?") - if not self.mainGui.askQuestion(qTitle, qText): + if not self.mainGui.askQuestion(question): return False docDup = DocDuplicator(self.theProject) @@ -1698,7 +1690,7 @@ class GuiProjectTree(QTreeWidget): dupCount += 1 if dupCount != nItems: - self.mainGui.makeAlert(self.tr("Could not duplicate all items."), nwAlert.WARN) + self.mainGui.makeAlert(self.tr("Could not duplicate all items."), level=nwAlert.WARN) self.saveTreeOrder() @@ -1757,7 +1749,7 @@ class GuiProjectTree(QTreeWidget): else: self.mainGui.makeAlert(self.tr( "There is nowhere to add item with name '{0}'." - ).format(nwItem.itemName), nwAlert.ERROR) + ).format(nwItem.itemName), level=nwAlert.ERROR) return None byIndex = -1 diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 6f1b0d16..81df33c2 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -310,7 +310,7 @@ class GuiManuscriptBuild(QDialog): self.buildProgress.setValue(0) bPath = Path(self.buildPath.text()) if not bPath.is_dir(): - self.mainGui.makeAlert(self.tr("Output folder does not exist."), nwAlert.ERROR) + self.mainGui.makeAlert(self.tr("Output folder does not exist."), level=nwAlert.ERROR) return False bExt = nwLabels.BUILD_EXT[bFormat] @@ -318,7 +318,6 @@ class GuiManuscriptBuild(QDialog): if buildPath.exists(): if not self.mainGui.askQuestion( - self.tr("File Exists"), self.tr("The file already exists. Do you want to overwrite it?") ): return False diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 8292ceb3..e767dced 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -270,11 +270,7 @@ class GuiManuscript(QDialog): """Delete the currently selected build settings entry.""" build = self._getSelectedBuild() if build is not None: - proceed = self.mainGui.askQuestion( - self.tr("Delete Build"), - self.tr("Delete build '{0}'?".format(build.name)) - ) - if proceed: + if self.mainGui.askQuestion(self.tr("Delete build '{0}'?".format(build.name))): self._builds.removeBuild(build.buildID) self._updateBuildsList() return diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index a3f22d2f..ed2c78b8 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -248,11 +248,7 @@ class GuiBuildSettings(QDialog): it's ok to reject them. """ if self._build.changed: - doSave = self.mainGui.askQuestion( - self.tr("Build Settings"), - self.tr("Do you want to save your changes?") - ) - if doSave: + if self.mainGui.askQuestion(self.tr("Do you want to save your changes?")): self._emitBuildData() self._build.resetChangedState() return diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index c2d1b892..c3b0b1b1 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -415,13 +415,15 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: - self.mainGui.makeAlert([ - self.tr("{0} file successfully written to:").format(textFmt), savePath - ], nwAlert.INFO) + self.mainGui.makeAlert( + self.tr("{0} file successfully written to:").format(textFmt), + info=savePath + ) else: - self.mainGui.makeAlert([ - self.tr("Failed to write {0} file.").format(textFmt), errMsg - ], nwAlert.ERROR) + self.mainGui.makeAlert( + self.tr("Failed to write {0} file.").format(textFmt), + info=errMsg, level=nwAlert.ERROR + ) return wSuccess diff --git a/tests/conftest.py b/tests/conftest.py index 744fdff5..42d0afb4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -145,10 +145,8 @@ def mockGUI(): @pytest.fixture(scope="function") def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" - monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) + monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) qtbot.addWidget(nwGUI) diff --git a/tests/mocked.py b/tests/mocked.py index 57381b0a..b9a64fb0 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -39,22 +39,22 @@ class MockGuiMain(QObject): # Test Variables self.askResponse = True self.lastAlert = "" - self.lastQuestion = ("", "") + self.lastQuestion = "" return def postLaunchTasks(self, cmdOpen): return - def makeAlert(self, message, level=0, exception=None): - assert isinstance(message, str) or isinstance(message, list) - print("%s: %s" % (str(level), message)) - self.lastAlert = str(message) + def makeAlert(self, text, info="", detals="", level=0, exception=None): + assert isinstance(text, str) + print("%s: %s" % (str(level), text)) + self.lastAlert = str(text) return - def askQuestion(self, title, qustion): - print("Question: %s" % qustion) - self.lastQuestion = (title, qustion) + def askQuestion(self, text, info="", details="", level=3): + print("Question: %s" % text) + self.lastQuestion = text return self.askResponse def setStatus(self, theMessage): diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 410d2e97..598f861e 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -224,7 +224,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mockGUI.askResponse = False assert theProject.openProject(fncPath) is False - assert "The file format of your project is about to be" in mockGUI.lastQuestion[1] + assert "The file format of your project is about to be" in mockGUI.lastQuestion mockGUI.askResponse = True # Won't open project from newer version @@ -232,7 +232,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) mockGUI.askResponse = False assert theProject.openProject(fncPath) is False - assert "This project was saved by a newer version" in mockGUI.lastQuestion[1] + assert "This project was saved by a newer version" in mockGUI.lastQuestion mockGUI.askResponse = True # Fail checking items should still pass @@ -249,7 +249,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): mockGUI.askResponse = True theProject.index._indexBroken = True assert theProject.openProject(fncPath) is True - assert "The file format of your project is about to be" in mockGUI.lastQuestion[1] + assert "The file format of your project is about to be" in mockGUI.lastQuestion assert theProject.index._indexBroken is False theProject.closeProject() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 17dc381f..5ab8ea25 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -114,7 +114,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, projPath): # Close project with monkeypatch.context() as mp: nwGUI.hasProject = True - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert nwGUI.newProject(projData={"projPath": projPath}) is False # No project path diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index 8c9cb849..a53ef837 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -34,12 +34,9 @@ LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW) @pytest.mark.skipif(not LANG_DATA, reason="No i18n Data") @pytest.mark.parametrize("language", [a for a, b in LANG_DATA]) def testI18n_Localisation(qtbot, monkeypatch, language, fncPath): - """test loading the gui with a specific language. - """ - monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Ok) - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + """Test loading the gui with a specific language.""" + monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) + monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) # Set the test langauge CONFIG.guiLocale = language diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 367ba3c7..c3cf85f7 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -642,7 +642,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): # The document isn't empty, so the message box should pop with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a, **k: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.No) assert not nwGUI.importDocument() assert nwGUI.docEditor.getText() == "Bar" @@ -655,16 +655,16 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): theMessage = "" - def recordMsg(*args): + def recordMsg(*args, **kwargs): nonlocal theMessage - theMessage = args[3] + theMessage = "%s|%s" % (args[0], kwargs["info"]) return None assert not theMessage - monkeypatch.setattr(QMessageBox, "information", recordMsg) + monkeypatch.setattr(nwGUI, "makeAlert", recordMsg) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) - theBits = theMessage.split("
") + theBits = theMessage.split("|") assert len(theBits) == 2 assert theBits[0] == "The currently open file is saved in:" assert theBits[1] == str(projPath / "content" / "000000000000f.nwd") diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 45e0ac04..bd645619 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -397,7 +397,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, # User cancels action with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert projTree.moveItemToTrash(C.hTitlePage) is False assert theProject.tree.isTrash(C.hTitlePage) is False @@ -444,7 +444,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro # User cancels action with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert projTree.permDeleteItem(C.hTitlePage) is False assert C.hTitlePage in theProject.tree @@ -496,7 +496,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock # User cancels with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert projTree.emptyTrash() is False assert C.hTitlePage in theProject.tree assert C.hChapterDir in theProject.tree @@ -617,7 +617,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Click no on the dialog with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) assert nwGUI.theProject.tree[hNewFolderOne].isFolderType() @@ -864,7 +864,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock # Duplicate title page, but select no with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert projTree._duplicateFromHandle(C.hTitlePage) is False assert len(nwGUI.theProject.tree) == 8 diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index d5105685..7e78f073 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -129,7 +129,7 @@ def testManuscriptBuild_Main( manus.buildPath.setText(str(fncPath)) manus.buildName.setText("TestBuild") with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert manus._runBuild() is False # Finish From 7756368113e764db61f33d80bf88625a0eb1546d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:25:51 +0200 Subject: [PATCH 05/10] Fix some minor issues with new alert code --- novelwriter/guimain.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 67e674e1..3ffd0768 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -137,6 +137,7 @@ class GuiMain(QMainWindow): # ============= # Sizes + iPx = self.mainTheme.fontPixelSize mPx = CONFIG.pxInt(4) hWd = CONFIG.pxInt(4) @@ -305,6 +306,15 @@ class GuiMain(QMainWindow): # Forward Functions self.setStatus = self.mainStatus.setStatus + # Cache Alert Pixmaps + pxSize = (2*iPx, 2*iPx) + self.alertPix: dict[nwAlert, QPixmap] = { + nwAlert.INFO: self.mainTheme.getPixmap("alert_info", pxSize), + nwAlert.WARN: self.mainTheme.getPixmap("alert_warn", pxSize), + nwAlert.ERROR: self.mainTheme.getPixmap("alert_error", pxSize), + nwAlert.ASK: self.mainTheme.getPixmap("alert_question", pxSize), + } + # Check that config loaded fine self.reportConfErr() @@ -317,16 +327,6 @@ class GuiMain(QMainWindow): # Handle Windows Mode self.showNormal() - # Cache Icons - - aPx = CONFIG.pxInt(48) - self.alertPix: dict[nwAlert, QPixmap] = { - nwAlert.INFO: self.mainTheme.getPixmap("alert_info", (aPx, aPx)), - nwAlert.WARN: self.mainTheme.getPixmap("alert_warn", (aPx, aPx)), - nwAlert.ERROR: self.mainTheme.getPixmap("alert_error", (aPx, aPx)), - nwAlert.ASK: self.mainTheme.getPixmap("alert_question", (aPx, aPx)), - } - logger.debug("Ready: GUI") if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: @@ -1109,7 +1109,7 @@ class GuiMain(QMainWindow): msgBox.setText(text) msgBox.setInformativeText(info) msgBox.setDetailedText(details) - msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msgBox.setStandardButtons(QMessageBox.Ok) msgBox.setIconPixmap(self.alertPix[level]) msgBox.adjustSize() msgBox.exec_() From 470017cdea6dc7be15ff9443b0c5d8a395f2ca21 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:45:25 +0200 Subject: [PATCH 06/10] Remove the global APP singleton again --- novelwriter/__init__.py | 5 +--- novelwriter/config.py | 63 ----------------------------------------- novelwriter/guimain.py | 4 +-- 3 files changed, 2 insertions(+), 70 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index d83415f7..bb42ba17 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -30,7 +30,7 @@ import logging from PyQt5.QtWidgets import QApplication, QErrorMessage from novelwriter.error import exceptionHandler, logException -from novelwriter.config import Config, NWApp +from novelwriter.config import Config ## # Version Scheme @@ -74,7 +74,6 @@ logger = logging.getLogger(__name__) # Create the global singleton instances CONFIG = Config() -APP = NWApp() def main(sysArgs: list | None = None): @@ -228,7 +227,6 @@ def main(sysArgs: list | None = None): from novelwriter.guimain import GuiMain if testMode: nwGUI = GuiMain() - APP.setGUI(nwGUI) return nwGUI else: @@ -248,7 +246,6 @@ def main(sysArgs: list | None = None): # Launch main GUI nwGUI = GuiMain() - APP.setGUI(nwGUI) nwGUI.postLaunchTasks(cmdOpen) sys.exit(nwApp.exec_()) diff --git a/novelwriter/config.py b/novelwriter/config.py index 7bbf125e..f7a94c1b 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -29,7 +29,6 @@ import json import logging from time import time -from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtGui import QFontDatabase @@ -42,11 +41,6 @@ from novelwriter.error import logException, formatException from novelwriter.common import checkPath, formatTimeStamp, NWConfigParser from novelwriter.constants import nwFiles, nwUnicode -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - from novelwriter.gui.theme import GuiTheme - from novelwriter.core.project import NWProject - logger = logging.getLogger(__name__) @@ -798,63 +792,6 @@ class Config: # END Class Config -class NWApp: - """Singleton: Global Pointers - - This class holds pointers to the core singletons: - * The GuiMain instance - * The GuiTheme instance - * The NWProject instance - """ - - def __init__(self) -> None: - self._gui: GuiMain | None = None - self._theme: GuiTheme | None = None - self._project: NWProject | None = None - return - - @property - def gui(self) -> GuiMain: - """The main gui instance.""" - if self._gui is None: - raise Exception("GLOBAL not fully initialised") - return self._gui - - @property - def theme(self) -> GuiTheme: - """The main gui theme instance.""" - if self._theme is None: - raise Exception("GLOBAL not fully initialised") - return self._theme - - @property - def project(self) -> NWProject: - """The main project instance.""" - if self._project is None: - raise Exception("GLOBAL not fully initialised") - return self._project - - def setGUI(self, gui: GuiMain) -> None: - """Set the GUI instance. Can only be set once.""" - if self._gui is None: - self._gui = gui - return - - def setTheme(self, theme: GuiTheme) -> None: - """Set the theme instance. Can only be set once.""" - if self._theme is None: - self._theme = theme - return - - def setProject(self, project: NWProject) -> None: - """Set the project instance. Can only be set once.""" - if self._project is None: - self._project = project - return - -# END Class NWApp - - class RecentProjects: def __init__(self, config): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 3ffd0768..6076d120 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( QStackedWidget, QVBoxLayout, QWidget ) -from novelwriter import CONFIG, APP, __hexversion__ +from novelwriter import CONFIG, __hexversion__ from novelwriter.gui.theme import GuiTheme from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.outline import GuiOutlineView @@ -115,8 +115,6 @@ class GuiMain(QMainWindow): # Core Classes self.mainTheme = GuiTheme() self.theProject = NWProject(self) - APP.setTheme(self.mainTheme) - APP.setProject(self.theProject) # Core Settings self.hasProject = False From 7cb77e8524d4ab11a2cbd3fa4708c4da412cd6f2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:49:04 +0200 Subject: [PATCH 07/10] Update comment --- novelwriter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index bb42ba17..242c7275 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -72,7 +72,7 @@ logger = logging.getLogger(__name__) # Main Program ## -# Create the global singleton instances +# Global config singleton CONFIG = Config() From abfe7ee7ff6fee9c9e2e8b470c5830069d78ccbc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:57:33 +0200 Subject: [PATCH 08/10] Clean up a few minor things --- novelwriter/config.py | 1 - novelwriter/constants.py | 1 - 2 files changed, 2 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index f7a94c1b..6bb94e52 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -4,7 +4,6 @@ novelWriter – Config Class File History: Created: 2018-09-22 [0.0.1] Config -Created: 2023-08-07 [2.1b2] NWGlobal This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen diff --git a/novelwriter/constants.py b/novelwriter/constants.py index c3abf581..f74ce319 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -23,7 +23,6 @@ along with this program. If not, see . """ from __future__ import annotations - from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP from novelwriter.enum import nwAlert, nwBuildFmt, nwItemClass, nwItemLayout, nwOutline From de66f1e21b12e2e367d61fe7b939c1a6b7758268 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 8 Aug 2023 09:33:42 +0200 Subject: [PATCH 09/10] Switch out some alert icons --- novelwriter/assets/icons/typicons_dark/icons.conf | 4 ++-- .../assets/icons/typicons_dark/typ_directions-full.svg | 4 ++++ .../assets/icons/typicons_dark/typ_info-large-full.svg | 4 ---- novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg | 2 +- novelwriter/assets/icons/typicons_dark/typ_warning-full.svg | 2 +- novelwriter/assets/icons/typicons_light/icons.conf | 4 ++-- .../assets/icons/typicons_light/typ_directions-full.svg | 4 ++++ .../assets/icons/typicons_light/typ_info-large-full.svg | 4 ---- .../assets/icons/typicons_light/typ_lightbulb-full.svg | 2 +- novelwriter/assets/icons/typicons_light/typ_warning-full.svg | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_directions-full.svg delete mode 100644 novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_directions-full.svg delete mode 100644 novelwriter/assets/icons/typicons_light/typ_info-large-full.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index 32a523f7..d0b69572 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -18,8 +18,8 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg alert_error = typ_delete-full.svg -alert_info = typ_info-large-full.svg -alert_question = typ_lightbulb-full.svg +alert_info = typ_lightbulb-full.svg +alert_question = typ_directions-full.svg alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg bookmark = typ_bookmark.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_directions-full.svg b/novelwriter/assets/icons/typicons_dark/typ_directions-full.svg new file mode 100644 index 00000000..0075285d --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_directions-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg b/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg deleted file mode 100644 index c9a13390..00000000 --- a/novelwriter/assets/icons/typicons_dark/typ_info-large-full.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg b/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg index 7397b602..569fc5e2 100644 --- a/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg +++ b/novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg @@ -1,4 +1,4 @@ - + diff --git a/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg b/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg index e6022e06..cdb68b1e 100644 --- a/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg +++ b/novelwriter/assets/icons/typicons_dark/typ_warning-full.svg @@ -1,4 +1,4 @@ - + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 4a2ce078..22fd9456 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -18,8 +18,8 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ [Map] add = typ_plus.svg alert_error = typ_delete-full.svg -alert_info = typ_info-large-full.svg -alert_question = typ_lightbulb-full.svg +alert_info = typ_lightbulb-full.svg +alert_question = typ_directions-full.svg alert_warn = typ_warning-full.svg backward = typ_chevron-left.svg bookmark = typ_bookmark.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_directions-full.svg b/novelwriter/assets/icons/typicons_light/typ_directions-full.svg new file mode 100644 index 00000000..230a2774 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_directions-full.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg b/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg deleted file mode 100644 index 80a43508..00000000 --- a/novelwriter/assets/icons/typicons_light/typ_info-large-full.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg b/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg index daf4254c..46b3d88a 100644 --- a/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg +++ b/novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg @@ -1,4 +1,4 @@ - + diff --git a/novelwriter/assets/icons/typicons_light/typ_warning-full.svg b/novelwriter/assets/icons/typicons_light/typ_warning-full.svg index d6a3b52c..3310b350 100644 --- a/novelwriter/assets/icons/typicons_light/typ_warning-full.svg +++ b/novelwriter/assets/icons/typicons_light/typ_warning-full.svg @@ -1,4 +1,4 @@ - + From 431bc8d40143690f4d7fea29ec8fedbeb377e7d3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 8 Aug 2023 10:39:10 +0200 Subject: [PATCH 10/10] Clarify closing dialog on build settings --- novelwriter/tools/manussettings.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index ed2c78b8..fb5eeba0 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -244,11 +244,14 @@ class GuiBuildSettings(QDialog): ## def _askToSaveBuild(self) -> None: - """Check if there are unsaved changes, and if there are, ask if - it's ok to reject them. + """Check if there are unsaved changes, and if there are, ask + whether the user wants to save them. """ if self._build.changed: - if self.mainGui.askQuestion(self.tr("Do you want to save your changes?")): + response = self.mainGui.askQuestion(self.tr( + "Do you want to save your changes to '{0}'?".format(self._build.name) + )) + if response: self._emitBuildData() self._build.resetChangedState() return