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