Rewrite the makeAlert and askQuestion functions to use recommended interface

This commit is contained in:
Veronica Berglyd Olsen
2023-08-07 20:19:51 +02:00
parent ec010bf18e
commit 90f9fa4451
9 changed files with 123 additions and 182 deletions
+4 -4
View File
@@ -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_())
-78
View File
@@ -1,78 +0,0 @@
"""
novelWriter Alert Functions
=============================
File History:
Created: 2023-08-07 [2.1b2]
This file is a part of novelWriter
Copyright 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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 = "<br>".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}<br>{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
@@ -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
@@ -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
+2 -2
View File
@@ -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:
+1
View File
@@ -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"),
+1
View File
@@ -125,6 +125,7 @@ class nwAlert(Enum):
INFO = 0
WARN = 1
ERROR = 2
ASK = 3
# END Enum nwAlert
+21 -17
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+90 -77
View File
@@ -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<br>%s" % (
self.tr("Close the current project?"),
self.tr("Changes are saved automatically.")
)
)
msgYes = self.askQuestion("%s<br>%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 = (
"<br>%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<br><br>%s<br>%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}<br>{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<br>%s" % (
self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.")
)
)
msgYes = self.askQuestion("%s<br>%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