diff --git a/nw/__init__.py b/nw/__init__.py index a66a888b..3373a34f 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -34,9 +34,10 @@ 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__ = "novelWriter" +__package__ = "nw" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" @@ -45,8 +46,9 @@ __hexversion__ = "0x001002f0" __date__ = "2020-07-29" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__status__ = "Pre-Release" -__url__ = "https://github.com/vkbo/novelWriter" +__status__ = "Beta" +__url__ = "https://novelwriter.io" +__sourceurl__ = "https://github.com/vkbo/novelWriter" __issuesurl__ = "https://github.com/vkbo/novelWriter/issues" __domain__ = "novelwriter.io" __docurl__ = "https://novelwriter.readthedocs.io" @@ -90,7 +92,6 @@ CONFIG = Config() def main(sysArgs=None): """Parses command line, sets up logging, and launches main GUI. """ - if sysArgs is None: sysArgs = sys.argv[1:] @@ -111,7 +112,7 @@ def main(sysArgs=None): ] helpMsg = ( - "{appname} {version} ({status} {date})\n" + "novelWriter {version} ({status} {date})\n" "{copyright}\n" "\n" "This program is distributed in the hope that it will be useful,\n" @@ -132,7 +133,6 @@ def main(sysArgs=None): " --data= Alternative user data path.\n" " --testmode Do not display GUI. Used by the test suite.\n" ).format( - appname = __package__, version = __version__, status = __status__, copyright = __copyright__, @@ -167,7 +167,9 @@ def main(sysArgs=None): print(helpMsg) sys.exit() elif inOpt in ("-v", "--version"): - print("%s %s Version %s [%s]" % (__package__,__status__,__version__,__date__)) + print("novelWriter %s Version %s [%s]" % ( + __status__, __version__, __date__) + ) sys.exit() elif inOpt == "--info": debugLevel = logging.INFO @@ -197,7 +199,7 @@ def main(sysArgs=None): CONFIG.cmdOpen = cmdOpen # Set Logging - logFmt = logging.Formatter(fmt=logFormat, datefmt="%Y-%m-%d %H:%M:%S", style="{") + logFmt = logging.Formatter(fmt=logFormat, style="{") if not logFile == "" and toFile: if path.isfile(logFile+".bak"): @@ -217,8 +219,8 @@ def main(sysArgs=None): logger.addHandler(cHandle) logger.setLevel(debugLevel) - logger.info("Starting %s %s (%s) %s" % ( - __package__, __version__, __hexversion__, __date__ + logger.info("Starting novelWriter %s (%s) %s" % ( + __version__, __hexversion__, __date__ )) # Check Packages and Versions @@ -249,13 +251,14 @@ def main(sysArgs=None): if errorData: errApp = QApplication([]) errMsg = QErrorMessage() - errMsg.setMinimumWidth(500) - errMsg.setMinimumHeight(300) + errMsg.resize(500, 300) errMsg.showMessage(( - "ERROR: %s 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 ...

" ) % ( - __package__, "
 - ".join(errorData) + "
 - ".join(errorData) )) errApp.exec_() sys.exit(1) @@ -268,13 +271,53 @@ def main(sysArgs=None): if testMode: nwGUI = GuiMain() return nwGUI + else: - nwApp = QApplication([__package__,("-style=%s" % qtStyle)]) - nwApp.setApplicationName(__package__) + nwApp = QApplication([CONFIG.appName, ("-style=%s" % qtStyle)]) + nwApp.setApplicationName(CONFIG.appName) nwApp.setApplicationVersion(__version__) nwApp.setWindowIcon(QIcon(CONFIG.appIcon)) - nwApp.setOrganizationDomain("novelwriter.io") - nwGUI = GuiMain() - sys.exit(nwApp.exec_()) + 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: + + from traceback import print_tb + from nw.error import formatHtmlErrMsg + + exType, exValue, exTrace = sys.exc_info() + + logger.critical("%s: %s" % (exType.__name__, str(exValue))) + print_tb(exTrace) + + 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) return diff --git a/nw/assets/icons/typicons_colour_dark/icons.conf b/nw/assets/icons/typicons_colour_dark/icons.conf index 33aeef93..c4167b90 100644 --- a/nw/assets/icons/typicons_colour_dark/icons.conf +++ b/nw/assets/icons/typicons_colour_dark/icons.conf @@ -9,8 +9,8 @@ [Main] name = Typicons Colour Dark description = Coulorised icons for dark GUI theme based on Typicons. -author = Stephen Hutchings (original), Veronica Berglyd Olsen (adaptation) -credit = Stephen Hutchings +author = Veronica Berglyd Olsen (adaptation) +credit = Stephen Hutchings (icon design) url = https://github.com/stephenhutchings/typicons.font license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/nw/assets/icons/typicons_colour_light/icons.conf b/nw/assets/icons/typicons_colour_light/icons.conf index 0ba6dd6b..be0b1853 100644 --- a/nw/assets/icons/typicons_colour_light/icons.conf +++ b/nw/assets/icons/typicons_colour_light/icons.conf @@ -9,8 +9,8 @@ [Main] name = Typicons Colour Light description = Coulorised icons for light GUI theme based on Typicons. -author = Stephen Hutchings (original), Veronica Berglyd Olsen (adaptation) -credit = Stephen Hutchings +author = Veronica Berglyd Olsen (adaptation) +credit = Stephen Hutchings (icon design) url = https://github.com/stephenhutchings/typicons.font license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/nw/assets/icons/typicons_grey_dark/icons.conf b/nw/assets/icons/typicons_grey_dark/icons.conf index 301c74ac..fba00697 100644 --- a/nw/assets/icons/typicons_grey_dark/icons.conf +++ b/nw/assets/icons/typicons_grey_dark/icons.conf @@ -9,8 +9,8 @@ [Main] name = Typicons Grey Dark description = Greyscaled icons for dark GUI theme based on Typicons. -author = Stephen Hutchings (original), Veronica Berglyd Olsen (adaptation) -credit = Stephen Hutchings +author = Veronica Berglyd Olsen (adaptation) +credit = Stephen Hutchings (icon design) url = https://github.com/stephenhutchings/typicons.font license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/nw/assets/icons/typicons_grey_light/icons.conf b/nw/assets/icons/typicons_grey_light/icons.conf index 6e98346a..62aba87f 100644 --- a/nw/assets/icons/typicons_grey_light/icons.conf +++ b/nw/assets/icons/typicons_grey_light/icons.conf @@ -9,8 +9,8 @@ [Main] name = Typicons Grey Light description = Greyscaled icons for light GUI theme based on Typicons. -author = Stephen Hutchings (original), Veronica Berglyd Olsen (adaptation) -credit = Stephen Hutchings +author = Veronica Berglyd Olsen (adaptation) +credit = Stephen Hutchings (icon design) url = https://github.com/stephenhutchings/typicons.font license = CC BY-SA 4.0 licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/nw/config.py b/nw/config.py index cbd79d17..ec397aea 100644 --- a/nw/config.py +++ b/nw/config.py @@ -52,8 +52,8 @@ class Config: def __init__(self): # Set Application Variables - self.appName = nw.__package__ - self.appHandle = nw.__package__.lower() + self.appName = "novelWriter" + self.appHandle = self.appName.lower() self.showGUI = True self.debugInfo = False self.cmdOpen = None diff --git a/nw/core/project.py b/nw/core/project.py index 496e8bc4..683affbe 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -368,23 +368,21 @@ class NWProject(): if fileVersion == "1.0": msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( - "The project file and data is created by a %s version lower than 0.7. " - "Do you want to upgrade the project to the most recent format?

" - "Note that after the upgrade, you cannot open the project with an older " - "version of %s any more, so make sure you have a recent backup." - ) % ( - nw.__package__, nw.__package__ + "The project file and data is created by a novelWriter version " + "lower than 0.7. Do you want to upgrade the project to the " + "most recent format?

Note that after the upgrade, you " + "cannot open the project with an older version of novelWriter " + "any more, so make sure you have a recent backup." )) if msgRes != QMessageBox.Yes: return False elif fileVersion != "1.1" and fileVersion != "1.2": self.makeAlert(( - "Unknown or unsupported {nw:s} project file format. " - "The project cannot be opened by this version of {nw:s}. " - "The file was saved with {nw:s} version {vers:s}." + "Unknown or unsupported novelWriter project file format. " + "The project cannot be opened by this version of novelWriter. " + "The file was saved with novelWriter version {vers:s}." ).format( - nw = nw.__package__, vers = appVersion, ), nwAlert.ERROR) return False @@ -395,11 +393,11 @@ class NWProject(): if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( - "This project was saved by a newer version of %s, version %s. This is version %s. " - "If you continue to open the project, some attributes and settings may not be " - "preserved. Continue opening the project?" + "This project was saved by a newer version of novelWriter, version %s. " + "This is version %s. If you continue to open the project, some attributes " + "and settings may not be preserved. Continue opening the project?" ) % ( - nw.__package__, appVersion, nw.__version__ + appVersion, nw.__version__ )) if msgRes != QMessageBox.Yes: return False 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/about.py b/nw/gui/about.py index d432a4d9..689a0aca 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -54,13 +54,13 @@ class GuiAbout(QDialog): self.innerBox = QHBoxLayout() self.innerBox.setSpacing(self.mainConf.pxInt(16)) - self.setWindowTitle("About %s" % nw.__package__) + self.setWindowTitle("About %s" % self.mainConf.appName) self.setMinimumWidth(self.mainConf.pxInt(650)) self.setMinimumHeight(self.mainConf.pxInt(600)) iPx = self.mainConf.pxInt(96) self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (iPx, iPx)) - self.lblName = QLabel("%s" % nw.__package__) + self.lblName = QLabel("%s" % self.mainConf.appName) self.lblVers = QLabel("v%s" % nw.__version__) self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x")) @@ -132,7 +132,7 @@ class GuiAbout(QDialog): "

Credits

" "

{credits:s}

" ).format( - name = nw.__package__, + name = self.mainConf.appName, copyright = nw.__copyright__, website = nw.__url__, domain = nw.__domain__, 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/gui/mainmenu.py b/nw/gui/mainmenu.py index 84a336eb..43b95385 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -258,7 +258,7 @@ class GuiMainMenu(QMenuBar): # Project > Exit self.aExitNW = QAction("Exit", self) - self.aExitNW.setStatusTip("Exit %s" % nw.__package__) + self.aExitNW.setStatusTip("Exit %s" % self.mainConf.appName) self.aExitNW.setShortcut("Ctrl+Q") self.aExitNW.triggered.connect(self._menuExit) self.projMenu.addAction(self.aExitNW) @@ -804,8 +804,8 @@ class GuiMainMenu(QMenuBar): self.helpMenu = self.addMenu("&Help") # Help > About - self.aAboutNW = QAction("About %s" % nw.__package__, self) - self.aAboutNW.setStatusTip("About %s" % nw.__package__) + self.aAboutNW = QAction("About %s" % self.mainConf.appName, self) + self.aAboutNW.setStatusTip("About %s" % self.mainConf.appName) self.aAboutNW.triggered.connect(self._showAbout) self.helpMenu.addAction(self.aAboutNW) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 0882b089..6bb39033 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -108,7 +108,7 @@ class GuiPreferences(PagedDialog): msgBox = QMessageBox() msgBox.information( self, "Preferences", - "Some changes will not be applied until %s has been restarted." % nw.__package__ + "Some changes will not be applied until novelWriter has been restarted." ) if validEntries: @@ -154,7 +154,7 @@ class GuiConfigEditGeneralTab(QWidget): self.mainForm.addRow( "Main GUI theme", self.selectTheme, - "Changing this requires restarting %s." % nw.__package__ + "Changing this requires restarting novelWriter." ) ## Select Icon Theme @@ -170,7 +170,7 @@ class GuiConfigEditGeneralTab(QWidget): self.mainForm.addRow( "Main icon theme", self.selectIcons, - "Changing this requires restarting %s." % nw.__package__ + "Changing this requires restarting novelWriter." ) ## Dark Icons @@ -193,7 +193,7 @@ class GuiConfigEditGeneralTab(QWidget): self.mainForm.addRow( "Font family", self.guiFont, - "Changing this requires restarting %s." % nw.__package__, + "Changing this requires restarting novelWriter.", theButton = self.fontButton ) @@ -206,7 +206,7 @@ class GuiConfigEditGeneralTab(QWidget): self.mainForm.addRow( "Font size", self.guiFontSize, - "Changing this requires restarting %s." % nw.__package__, + "Changing this requires restarting novelWriter.", theUnit = "pt" ) diff --git a/nw/guimain.py b/nw/guimain.py index f2a4cb3f..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 @@ -220,7 +221,7 @@ class GuiMain(QMainWindow): else: self.manageProjects() - logger.debug("%s is ready ..." % nw.__package__) + logger.debug("novelWriter is ready ...") return @@ -384,13 +385,13 @@ class GuiMain(QMainWindow): msgBox = QMessageBox() msgRes = msgBox.warning( self, "Project Locked", ( - "The project is already open by another instance of %s, and is " - "therefore locked. Override lock and continue anyway?

" + "The project is already open by another instance of novelWriter, and " + "is therefore locked. Override lock and continue anyway?

" "Note: If the program or the computer previously crashed, the lock " - "can safely be overridden. If, however, another instance of %s has " - "the project open, overriding the lock may corrupt the project, and " - "is not recommended.%s" - ) % (nw.__package__, nw.__package__, lockDetails), + "can safely be overridden. If, however, another instance of " + "novelWriter has the project open, overriding the lock may corrupt " + "the project, and is not recommended.%s" + ) % lockDetails, QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if msgRes == QMessageBox.Yes: @@ -872,7 +873,7 @@ class GuiMain(QMainWindow): if msgRes != QMessageBox.Yes: return False - logger.info("Exiting %s" % nw.__package__) + logger.info("Exiting novelWriter") if not self.isFocusMode: self.mainConf.setMainPanePos(self.splitMain.sizes()) @@ -1037,7 +1038,7 @@ class GuiMain(QMainWindow): return True def _setWindowTitle(self, projName=None): - winTitle = "%s" % nw.__package__ + winTitle = self.mainConf.appName if projName is not None: winTitle += " - %s" % projName self.setWindowTitle(winTitle) diff --git a/setup.py b/setup.py index 091e3652..6638e0bc 100755 --- a/setup.py +++ b/setup.py @@ -1,19 +1,24 @@ #!/usr/bin/env python3 import setuptools +from nw import __version__, __url__, __docurl__, __issuesurl__, __sourceurl__ + with open("README.md", "r") as inFile: - long_description = inFile.read() + longDescription = inFile.read() + +with open("requirements.txt", "r") as inFile: + pkgRequirements = inFile.read().strip().splitlines() setuptools.setup( name = "novelWriter", - version = "0.10.2", + version = __version__, author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", - long_description = long_description, + long_description = longDescription, long_description_content_type = "text/markdown", license = "GNU General Public License v3", - url = "https://github.com/vkbo/novelWriter", + url = __url__, entry_points = { "console_scripts" : ["novelWriter-cli=nw:main"], "gui_scripts" : ["novelWriter=nw:main"], @@ -22,26 +27,24 @@ setuptools.setup( include_package_data = True, package_data = {"": ["*.conf"]}, project_urls = { - "Bug Tracker": "https://github.com/vkbo/novelWriter/issues", - "Documentation": "https://novelwriter.readthedocs.io/", - "Source Code": "https://github.com/vkbo/novelWriter", + "Bug Tracker": __issuesurl__, + "Documentation": __docurl__, + "Source Code": __sourceurl__, }, classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: Implementation :: CPython", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Operating System :: OS Independent", "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Topic :: Text Editors", ], python_requires = ">=3.6", - install_requires = [ - "pyqt5>=5.2.1", - "lxml>=4.2.0", - "pyenchant>=3.0.0", - ], + install_requires = pkgRequirements, )