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