diff --git a/nw/__init__.py b/nw/__init__.py index 2271157a..5ffae592 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -34,6 +34,7 @@ from os import path, remove, rename from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage +from nw.error import exceptionHandler from nw.config import Config __package__ = "nw" @@ -249,11 +250,12 @@ def main(sysArgs=None): if errorData: errApp = QApplication([]) errMsg = QErrorMessage() - errMsg.setMinimumWidth(500) - errMsg.setMinimumHeight(300) + errMsg.resize(500, 300) errMsg.showMessage(( - "ERROR: novelWriter cannot start due to the following issues:

" - " - %s

Exiting." + "

A critical error has been encountered

" + "

novelWriter cannot start due to the following issues:

" + "

 - %s

" + "

Shutting down ...

" ) % ( "
 - ".join(errorData) )) @@ -276,34 +278,44 @@ def main(sysArgs=None): nwApp.setWindowIcon(QIcon(CONFIG.appIcon)) nwApp.setOrganizationDomain(__domain__) + # We try to catch critical errors while setting up the main GUI + # by wrapping the main GUI in a try/except structure. This will + # not catch all exceptions for other parts of the application. + # For all other unhandled exceptions, we use a custom exception + # handler that pops a dialog box with the error message. + sys.excepthook = exceptionHandler + try: nwGUI = GuiMain() sys.exit(nwApp.exec_()) except Exception: - # novelWriter has crashed! - from traceback import print_tb, format_tb - eInfo = sys.exc_info() - logger.critical("%s: %s" % (eInfo[0].__name__, eInfo[1])) - print_tb(eInfo[2]) + from traceback import print_tb + from nw.error import formatHtmlErrMsg - del nwApp + exType, exValue, exTrace = sys.exc_info() - errApp = QApplication([]) - errMsg = QErrorMessage() - errMsg.setWindowTitle("Critical Error") - errMsg.setMinimumWidth(500) - errMsg.setMinimumHeight(300) - errMsg.showMessage(( - "

novelWriter has encountered a critical error!

" - "

%s:
%s

" - "

Traceback:
%s

" - "

Shutting down ...

" - ) % (eInfo[0].__name__, eInfo[1], "
".join(format_tb(eInfo[2])))) - errApp.exec_() + logger.critical("%s: %s" % (exType.__name__, str(exValue))) + print_tb(exTrace) - del eInfo + try: + del nwApp + + errApp = QApplication([]) + errMsg = QErrorMessage() + errMsg.setWindowTitle("Critical Error") + errMsg.resize(800, 400) + errMsg.showMessage(( + "

A critical error has been encountered

" + "%s" + "

Shutting down ...

" + ) % formatHtmlErrMsg(exType, exValue, exTrace)) + errApp.exec_() + + except Exception as e: + logger.critical("Could not create error message dialog.") + logger.critical(str(e)) sys.exit(1) diff --git a/nw/error.py b/nw/error.py new file mode 100644 index 00000000..0301f3ba --- /dev/null +++ b/nw/error.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +"""novelWriter Init + + novelWriter – Exception Handling +================================== + Error handling functions + + File History: + Created: 2020-08-02 [0.10.2] + + This file is a part of novelWriter + Copyright 2020, 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 . +""" + +def formatHtmlErrMsg(exType, exValue, exTrace): + """Generates a HTML version of an exception. + """ + try: + import sys + from traceback import format_tb + from nw import __issuesurl__, __version__ + from PyQt5.Qt import PYQT_VERSION_STR + from PyQt5.QtCore import QT_VERSION_STR, QSysInfo + + fmtTrace = "" + for trEntry in format_tb(exTrace): + for trLine in trEntry.split("\n"): + stripLine = trLine.lstrip(" ") + nIndent = len(trLine) - len(stripLine) + fmtTrace += " "*nIndent + stripLine + "
" + + theMessage = ( + "

Please report this error by submitting an issue report on " + "GitHub, providing a description and this error message. " + "URL: <{issueUrl}>.

" + "

Environment
Version: {nwVersion}, OS: {osType} ({osKernel})," + "Python: {pyVersion} ({pyHexVer:#x}), Qt: {qtVers}, PyQt: {pyqtVers}

" + "

Error Type
{exType}: {exMessage}

" + "

Traceback
{exTrace}

" + ).format( + nwVersion = __version__, + osType = sys.platform, + osKernel = QSysInfo.kernelVersion(), + pyVersion = sys.version.split()[0], + pyHexVer = sys.hexversion, + qtVers = QT_VERSION_STR, + pyqtVers = PYQT_VERSION_STR, + issueUrl = __issuesurl__, + exType = exType.__name__, + exMessage = str(exValue), + exTrace = fmtTrace + ) + + return theMessage + + except Exception as e: + return "Could not generate error message.
%s" % str(e) + + return "Could not generate error message." + + +def exceptionHandler(exType, exValue, exTrace): + """Function to catch unhandled global exceptions. + """ + import logging + from traceback import print_tb, format_tb + from nw import CONFIG + from PyQt5.QtWidgets import qApp, QApplication, QErrorMessage, QMessageBox + + logger = logging.getLogger(__name__) + logger.error("%s: %s" % (exType.__name__, str(exValue))) + print_tb(exTrace) + + if not CONFIG.showGUI: + return + + try: + nwGUI = None + for qWin in qApp.topLevelWidgets(): + if qWin.objectName() == "GuiMain": + nwGUI = qWin + break + + if nwGUI is None: + logger.warning("Could not find main GUI window so cannot open error dialog") + return + + errMsg = QErrorMessage(nwGUI) + errMsg.setWindowTitle("Unhandled Error") + errMsg.resize(800, 400) + errMsg.showMessage(( + "

An unhandled error has been encountered

%s" + ) % formatHtmlErrMsg(exType, exValue, exTrace)) + + except Exception as e: + logger.error(str(e)) + + return diff --git a/nw/gui/build.py b/nw/gui/build.py index 34a01f05..93aa8271 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -432,7 +432,6 @@ class GuiBuildNovel(QDialog): def _buildPreview(self): """Build a preview of the project in the document viewer. """ - # Get Settings fmtTitle = self.fmtTitle.text().strip() fmtChapter = self.fmtChapter.text().strip() diff --git a/nw/guimain.py b/nw/guimain.py index 6ea48c9b..12655f20 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -56,6 +56,7 @@ class GuiMain(QMainWindow): QMainWindow.__init__(self) logger.debug("Initialising GUI ...") + self.setObjectName("GuiMain") self.mainConf = nw.CONFIG # Some runtime info useful for debugging