Improve alert boxes (#1501)

This commit is contained in:
Veronica Berglyd Olsen
2023-08-08 09:45:58 +01:00
committed by GitHub
36 changed files with 307 additions and 304 deletions
+4 -4
View File
@@ -21,6 +21,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import sys import sys
import getopt import getopt
@@ -71,13 +72,12 @@ logger = logging.getLogger(__name__)
# Main Program # Main Program
## ##
# Load the main config as a global object # Global config singleton
CONFIG = Config() CONFIG = Config()
def main(sysArgs=None): def main(sysArgs: list | None = None):
"""Parse command line, set up logging, and launch main GUI. """Parse command line, set up logging, and launch main GUI."""
"""
if sysArgs is None: if sysArgs is None:
sysArgs = sys.argv[1:] sysArgs = sys.argv[1:]
@@ -17,6 +17,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map] [Map]
add = typ_plus.svg add = typ_plus.svg
alert_error = typ_delete-full.svg
alert_info = typ_lightbulb-full.svg
alert_question = typ_directions-full.svg
alert_warn = typ_warning-full.svg
backward = typ_chevron-left.svg backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg bookmark = typ_bookmark.svg
browse = typ_folder-open.svg browse = typ_folder-open.svg
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 0c-6.6285 0-12 5.373-12 12s5.3715 12 12 12 12-5.373 12-12-5.3715-12-12-12zm5.5605 15.44c0.5865 0.5865 0.5865 1.5345 0 2.121-0.2925 0.2925-0.6765 0.4395-1.0605 0.4395s-0.768-0.147-1.0605-0.4395l-3.4395-3.4395-3.4395 3.4395c-0.2925 0.2925-0.6765 0.4395-1.0605 0.4395s-0.768-0.147-1.0605-0.4395c-0.5865-0.5865-0.5865-1.5345 0-2.121l3.4395-3.4395-3.4395-3.4395c-0.5865-0.5865-0.5865-1.5345 0-2.121s1.5345-0.5865 2.121 0l3.4395 3.4395 3.4395-3.4395c0.5865-0.5865 1.5345-0.5865 2.121 0s0.5865 1.5345 0 2.121l-3.4395 3.4395z" fill="#d64848" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m24 7.3333-3.6909-3.476c-0.76122-0.72133-1.9392-1.1907-2.998-1.1907h-3.9093v-0.66667c0-1.104-0.9006-2-2.0103-2-1.1097 0-2.0103 0.896-2.0103 2v0.66667h-4.6906c-2.5866 0-4.6906 2.0933-4.6906 4.6667 0 1.8613 1.1097 3.4613 2.7018 4.2093l-2.4713 2.4573 3.4322 3.4147c0.7639 0.76 1.9567 1.252 3.0395 1.252h2.9484l1.0721 5.3333h1.3402l1.0721-5.3333h3.6185c2.5879 0 4.6906-2.0933 4.6906-4.6667 0-1.2027-0.47308-2.2907-1.2263-3.1187l0.09649-0.07467zm-7.2477 8.6667h-10.051c-0.36855 0-0.88184-0.21067-1.1445-0.472l-1.5358-1.528 1.5358-1.528c0.26134-0.26 0.77329-0.472 1.1445-0.472h10.051c1.1097 0 2.0103 0.896 2.0103 2s-0.9006 2-2.0103 2zm1.7141-7.1253c-0.2667 0.25333-0.78535 0.45867-1.1552 0.45867h-12.621c-1.1097 0-2.0103-0.896-2.0103-2s0.9006-2 2.0103-2h12.621c0.36989 0 0.88854 0.20533 1.1552 0.45867l1.6364 1.5413z" fill="#69c" stroke-width="1.3368"/>
</svg>

After

Width:  |  Height:  |  Size: 1002 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 5.1429c-0.31543 0-0.57143 0.256-0.57143 0.57143s0.256 0.57143 0.57143 0.57143c1.2377 0 2.2446 1.0069 2.2446 2.2446 0 0.31543 0.256 0.57143 0.57143 0.57143s0.57143-0.256 0.57143-0.57143c0-1.8674-1.52-3.3874-3.3874-3.3874zm0-5.1429c-4.7269 0-8.5714 3.8446-8.5714 8.5714 0 1.6983 0.50286 3.3394 1.456 4.76l0.091429 0.15428c2.0857 2.9783 2.4526 3.92 2.4526 4.8v3.4286c0 0.63086 0.512 1.1429 1.1429 1.1429h2.2857c0 0.29714 0.12572 0.59429 0.33143 0.81143 0.21714 0.20572 0.51429 0.33143 0.81143 0.33143s0.59428-0.12571 0.81143-0.33143c0.20571-0.21714 0.33143-0.51428 0.33143-0.81143h2.2857c0.63086 0 1.1429-0.512 1.1429-1.1429v-3.4286c0-0.89371 0.36457-1.84 2.4366-4.7989 1.0229-1.4571 1.5634-3.1566 1.5634-4.9154 0-4.7269-3.8446-8.5714-8.5714-8.5714zm2.2857 20.571h-4.5714v-1.1429h4.5714zm2.8514-8.3966c-1.6754 2.392-2.4491 3.7589-2.7257 4.968h-1.2686v-2.2857c0-0.63086-0.512-1.1429-1.1429-1.1429-0.63086 0-1.1429 0.512-1.1429 1.1429v2.2857h-1.272c-0.27428-1.1771-1.0263-2.5143-2.6354-4.8229l-0.088-0.14743c-0.75086-1.0674-1.1474-2.3131-1.1474-3.6011 0-3.4663 2.8194-6.2857 6.2857-6.2857s6.2857 2.8194 6.2857 6.2857c0 1.2869-0.39657 2.5326-1.1486 3.6034z" fill="#69c" stroke-width="1.1429"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m23.197 15.837-7.2179-12.031c-0.94741-1.5786-2.3966-2.4821-3.9789-2.4821s-3.0315 0.90347-3.9789 2.4796l-7.2179 12.033c-0.95962 1.5981-1.0646 3.3025-0.28691 4.6761 0.77648 1.3747 2.2928 2.1634 4.1583 2.1634h14.651c1.8655 0 3.3819-0.7887 4.1583-2.1622 0.7777-1.3735 0.67271-3.0778-0.28691-4.6772zm-11.197 2.6262c-1.0426 0-1.8924-0.84852-1.8924-1.8912 0-1.0439 0.84852-1.8936 1.8924-1.8936 1.0439 0 1.8924 0.84975 1.8924 1.8936 0 1.0426-0.84974 1.8912-1.8924 1.8912zm1.9937-9.064c-0.01343 0.037847-1.7105 4.2341-1.7105 4.2341-0.04639 0.11477-0.15872 0.19046-0.28203 0.19046s-0.23563-0.0757-0.28202-0.19046l-1.6983-4.1974c-0.10988-0.28447-0.15749-0.54085-0.15749-0.79969 0-1.1782 0.9584-2.1366 2.1366-2.1366s2.1366 0.95836 2.1366 2.1366c0 0.25883-0.04762 0.51522-0.14284 0.76306z" fill="#f99157" stroke-width="1.2209"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

@@ -17,6 +17,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map] [Map]
add = typ_plus.svg add = typ_plus.svg
alert_error = typ_delete-full.svg
alert_info = typ_lightbulb-full.svg
alert_question = typ_directions-full.svg
alert_warn = typ_warning-full.svg
backward = typ_chevron-left.svg backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg bookmark = typ_bookmark.svg
browse = typ_folder-open.svg browse = typ_folder-open.svg
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 0c-6.6285 0-12 5.373-12 12s5.3715 12 12 12 12-5.373 12-12-5.3715-12-12-12zm5.5605 15.44c0.5865 0.5865 0.5865 1.5345 0 2.121-0.2925 0.2925-0.6765 0.4395-1.0605 0.4395s-0.768-0.147-1.0605-0.4395l-3.4395-3.4395-3.4395 3.4395c-0.2925 0.2925-0.6765 0.4395-1.0605 0.4395s-0.768-0.147-1.0605-0.4395c-0.5865-0.5865-0.5865-1.5345 0-2.121l3.4395-3.4395-3.4395-3.4395c-0.5865-0.5865-0.5865-1.5345 0-2.121s1.5345-0.5865 2.121 0l3.4395 3.4395 3.4395-3.4395c0.5865-0.5865 1.5345-0.5865 2.121 0s0.5865 1.5345 0 2.121l-3.4395 3.4395z" fill="#c82829" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m24 7.3333-3.6909-3.476c-0.76122-0.72133-1.9392-1.1907-2.998-1.1907h-3.9093v-0.66667c0-1.104-0.9006-2-2.0103-2-1.1097 0-2.0103 0.896-2.0103 2v0.66667h-4.6906c-2.5866 0-4.6906 2.0933-4.6906 4.6667 0 1.8613 1.1097 3.4613 2.7018 4.2093l-2.4713 2.4573 3.4322 3.4147c0.7639 0.76 1.9567 1.252 3.0395 1.252h2.9484l1.0721 5.3333h1.3402l1.0721-5.3333h3.6185c2.5879 0 4.6906-2.0933 4.6906-4.6667 0-1.2027-0.47308-2.2907-1.2263-3.1187l0.09649-0.07467zm-7.2477 8.6667h-10.051c-0.36855 0-0.88184-0.21067-1.1445-0.472l-1.5358-1.528 1.5358-1.528c0.26134-0.26 0.77329-0.472 1.1445-0.472h10.051c1.1097 0 2.0103 0.896 2.0103 2s-0.9006 2-2.0103 2zm1.7141-7.1253c-0.2667 0.25333-0.78535 0.45867-1.1552 0.45867h-12.621c-1.1097 0-2.0103-0.896-2.0103-2s0.9006-2 2.0103-2h12.621c0.36989 0 0.88854 0.20533 1.1552 0.45867l1.6364 1.5413z" fill="#4271ae" stroke-width="1.3368"/>
</svg>

After

Width:  |  Height:  |  Size: 1005 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 5.1429c-0.31543 0-0.57143 0.256-0.57143 0.57143s0.256 0.57143 0.57143 0.57143c1.2377 0 2.2446 1.0069 2.2446 2.2446 0 0.31543 0.256 0.57143 0.57143 0.57143s0.57143-0.256 0.57143-0.57143c0-1.8674-1.52-3.3874-3.3874-3.3874zm0-5.1429c-4.7269 0-8.5714 3.8446-8.5714 8.5714 0 1.6983 0.50286 3.3394 1.456 4.76l0.091429 0.15428c2.0857 2.9783 2.4526 3.92 2.4526 4.8v3.4286c0 0.63086 0.512 1.1429 1.1429 1.1429h2.2857c0 0.29714 0.12572 0.59429 0.33143 0.81143 0.21714 0.20572 0.51429 0.33143 0.81143 0.33143s0.59428-0.12571 0.81143-0.33143c0.20571-0.21714 0.33143-0.51428 0.33143-0.81143h2.2857c0.63086 0 1.1429-0.512 1.1429-1.1429v-3.4286c0-0.89371 0.36457-1.84 2.4366-4.7989 1.0229-1.4571 1.5634-3.1566 1.5634-4.9154 0-4.7269-3.8446-8.5714-8.5714-8.5714zm2.2857 20.571h-4.5714v-1.1429h4.5714zm2.8514-8.3966c-1.6754 2.392-2.4491 3.7589-2.7257 4.968h-1.2686v-2.2857c0-0.63086-0.512-1.1429-1.1429-1.1429-0.63086 0-1.1429 0.512-1.1429 1.1429v2.2857h-1.272c-0.27428-1.1771-1.0263-2.5143-2.6354-4.8229l-0.088-0.14743c-0.75086-1.0674-1.1474-2.3131-1.1474-3.6011 0-3.4663 2.8194-6.2857 6.2857-6.2857s6.2857 2.8194 6.2857 6.2857c0 1.2869-0.39657 2.5326-1.1486 3.6034z" fill="#4271ae" stroke-width="1.1429"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m23.197 15.837-7.2179-12.031c-0.94741-1.5786-2.3966-2.4821-3.9789-2.4821s-3.0315 0.90347-3.9789 2.4796l-7.2179 12.033c-0.95962 1.5981-1.0646 3.3025-0.28691 4.6761 0.77648 1.3747 2.2928 2.1634 4.1583 2.1634h14.651c1.8655 0 3.3819-0.7887 4.1583-2.1622 0.7777-1.3735 0.67271-3.0778-0.28691-4.6772zm-11.197 2.6262c-1.0426 0-1.8924-0.84852-1.8924-1.8912 0-1.0439 0.84852-1.8936 1.8924-1.8936 1.0439 0 1.8924 0.84975 1.8924 1.8936 0 1.0426-0.84974 1.8912-1.8924 1.8912zm1.9937-9.064c-0.01343 0.037847-1.7105 4.2341-1.7105 4.2341-0.04639 0.11477-0.15872 0.19046-0.28203 0.19046s-0.23563-0.0757-0.28202-0.19046l-1.6983-4.1974c-0.10988-0.28447-0.15749-0.54085-0.15749-0.79969 0-1.1782 0.9584-2.1366 2.1366-2.1366s2.1366 0.95836 2.1366 2.1366c0 0.25883-0.04762 0.51522-0.14284 0.76306z" fill="#f5871f" stroke-width="1.2209"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

+1 -3
View File
@@ -1,10 +1,9 @@
""" """
novelWriter Config Class novelWriter Config Class
========================== ==========================
Class holding the user preferences and handling the config file
File History: File History:
Created: 2018-09-22 [0.0.1] Created: 2018-09-22 [0.0.1] Config
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen Copyright 20182023, Veronica Berglyd Olsen
@@ -22,7 +21,6 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import sys import sys
+7 -2
View File
@@ -1,7 +1,6 @@
""" """
novelWriter Constants novelWriter Constants
======================= =======================
Constants and maps for translating flags and enums to text
File History: File History:
Created: 2019-04-28 [0.0.1] Created: 2019-04-28 [0.0.1]
@@ -26,7 +25,7 @@ from __future__ import annotations
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP 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: def trConst(text: str) -> str:
@@ -163,6 +162,12 @@ class nwLabels:
nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"), nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"),
nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"), 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"),
nwAlert.ASK: QT_TRANSLATE_NOOP("Constant", "Question"),
}
ITEM_DESCRIPTION = { ITEM_DESCRIPTION = {
"none": QT_TRANSLATE_NOOP("Constant", "None"), "none": QT_TRANSLATE_NOOP("Constant", "None"),
"root": QT_TRANSLATE_NOOP("Constant", "Root Folder"), "root": QT_TRANSLATE_NOOP("Constant", "Root Folder"),
+2 -2
View File
@@ -479,7 +479,7 @@ class ProjectBuilder:
except Exception as exc: except Exception as exc:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Failed to create a new example project." "Failed to create a new example project."
), nwAlert.ERROR, exception=exc) ), level=nwAlert.ERROR, exception=exc)
return False return False
else: else:
@@ -487,7 +487,7 @@ class ProjectBuilder:
"Failed to create a new example project. " "Failed to create a new example project. "
"Could not find the necessary files. " "Could not find the necessary files. "
"They seem to be missing from this installation." "They seem to be missing from this installation."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
return True return True
+28 -33
View File
@@ -173,9 +173,10 @@ class NWProject(QObject):
if self._tree.checkType(tHandle, nwItemType.FILE): if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = self._storage.getDocument(tHandle) delDoc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument(): if not delDoc.deleteDocument():
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("Could not delete document file."), delDoc.getError() self.tr("Could not delete document file."),
], nwAlert.ERROR) info=delDoc.getError(), level=nwAlert.ERROR
)
return False return False
self._index.deleteHandle(tHandle) self._index.deleteHandle(tHandle)
@@ -227,7 +228,7 @@ class NWProject(QObject):
if not self._storage.openProjectInPlace(projPath): if not self._storage.openProjectInPlace(projPath):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Could not open project with path: {0}" "Could not open project with path: {0}"
).format(projPath), nwAlert.ERROR) ).format(projPath), level=nwAlert.ERROR)
return False return False
# Project Lock # Project Lock
@@ -266,17 +267,17 @@ class NWProject(QObject):
if xmlReader.state == XMLReadState.NOT_NWX_FILE: if xmlReader.state == XMLReadState.NOT_NWX_FILE:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Project file does not appear to be a novelWriterXML file." "Project file does not appear to be a novelWriterXML file."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
elif xmlReader.state == XMLReadState.UNKNOWN_VERSION: elif xmlReader.state == XMLReadState.UNKNOWN_VERSION:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Unknown or unsupported novelWriter project file format. " "Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. " "The project cannot be opened by this version of novelWriter. "
"The file was saved with novelWriter version {0}." "The file was saved with novelWriter version {0}."
).format(appVersion), nwAlert.ERROR) ).format(appVersion), level=nwAlert.ERROR)
else: else:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Failed to parse project xml." "Failed to parse project xml."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
self.clearProject() self.clearProject()
return False return False
@@ -285,14 +286,11 @@ class NWProject(QObject):
# ==================== # ====================
if xmlReader.state == XMLReadState.WAS_LEGACY: if xmlReader.state == XMLReadState.WAS_LEGACY:
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(self.tr(
self.tr("File Version"), "The file format of your project is about to be updated. "
self.tr( "If you proceed, older versions of novelWriter will no "
"The file format of your project is about to be updated. " "longer be able to open this project. Continue?"
"If you proceed, older versions of novelWriter will no " ))
"longer be able to open this project. Continue?"
)
)
if not msgYes: if not msgYes:
self.clearProject() self.clearProject()
return False return False
@@ -301,16 +299,13 @@ class NWProject(QObject):
# ========================= # =========================
if xmlReader.hexVersion > hexToInt(__hexversion__): if xmlReader.hexVersion > hexToInt(__hexversion__):
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(self.tr(
self.tr("Version Conflict"), "This project was saved by a newer version of "
self.tr( "novelWriter, version {0}. This is version {1}. If you "
"This project was saved by a newer version of " "continue to open the project, some attributes and "
"novelWriter, version {0}. This is version {1}. If you " "settings may not be preserved, but the overall project "
"continue to open the project, some attributes and " "should be fine. Continue opening the project?"
"settings may not be preserved, but the overall project " ).format(appVersion, __version__))
"should be fine. Continue opening the project?"
).format(appVersion, __version__)
)
if not msgYes: if not msgYes:
self.clearProject() self.clearProject()
return False return False
@@ -333,7 +328,7 @@ class NWProject(QObject):
if orphans > 0: if orphans > 0:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Found {0} orphaned file(s) in the project. {1} file(s) were recovered." "Found {0} orphaned file(s) in the project. {1} file(s) were recovered."
).format(orphans, recovered), nwAlert.WARN) ).format(orphans, recovered), level=nwAlert.WARN)
self._index.loadIndex() self._index.loadIndex()
if xmlReader.state == XMLReadState.WAS_LEGACY: if xmlReader.state == XMLReadState.WAS_LEGACY:
@@ -357,7 +352,7 @@ class NWProject(QObject):
if not self._storage.isOpen(): if not self._storage.isOpen():
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"There is no project open." "There is no project open."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
saveTime = time() saveTime = time()
@@ -382,7 +377,7 @@ class NWProject(QObject):
if not xmlWriter.write(self._data, content, saveTime, editTime): if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Failed to save project." "Failed to save project."
), nwAlert.ERROR, exception=xmlWriter.error) ), level=nwAlert.ERROR, exception=xmlWriter.error)
return False return False
# Save other project data # Save other project data
@@ -426,14 +421,14 @@ class NWProject(QObject):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot backup project because no valid backup path is set. " "Cannot backup project because no valid backup path is set. "
"Please set a valid backup location in Preferences." "Please set a valid backup location in Preferences."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
if not self._data.name: if not self._data.name:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot backup project because no project name is set. " "Cannot backup project because no project name is set. "
"Please set a Project Name in Project Settings." "Please set a Project Name in Project Settings."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
cleanName = makeFileNameSafe(self._data.name) cleanName = makeFileNameSafe(self._data.name)
@@ -443,7 +438,7 @@ class NWProject(QObject):
except Exception as exc: except Exception as exc:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Could not create backup folder." "Could not create backup folder."
), nwAlert.ERROR, exception=exc) ), level=nwAlert.ERROR, exception=exc)
return False return False
timeStamp = formatTimeStamp(time(), fileSafe=True) timeStamp = formatTimeStamp(time(), fileSafe=True)
@@ -453,11 +448,11 @@ class NWProject(QObject):
if doNotify: if doNotify:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Backup archive file written to: {0} [{1}B]" "Backup archive file written to: {0} [{1}B]"
).format(str(archName), formatInt(size)), nwAlert.INFO) ).format(str(archName), formatInt(size)))
else: else:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Could not write backup archive." "Could not write backup archive."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
self.mainGui.setStatus(self.tr( self.mainGui.setStatus(self.tr(
+4 -7
View File
@@ -225,13 +225,10 @@ class GuiProjectLoad(QDialog):
selList = self.listBox.selectedItems() selList = self.listBox.selectedItems()
if selList: if selList:
projName = selList[0].text(self.C_NAME) projName = selList[0].text(self.C_NAME)
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(self.tr(
self.tr("Remove Entry"), "Remove '{0}' from the recent projects list? "
self.tr( "The project files will not be deleted."
"Remove '{0}' from the recent projects list? " ).format(projName))
"The project files will not be deleted."
).format(projName)
)
if msgYes: if msgYes:
CONFIG.recentProjects.remove( CONFIG.recentProjects.remove(
selList[0].data(self.C_NAME, self.D_PATH) selList[0].data(self.C_NAME, self.D_PATH)
+8 -3
View File
@@ -1,7 +1,6 @@
""" """
novelWriter GUI Project Settings novelWriter GUI Project Settings
================================== ==================================
GUI classes for the project settings dialog
File History: File History:
Created: 2018-09-29 [0.0.1] Created: 2018-09-29 [0.0.1]
@@ -22,9 +21,12 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtGui import QIcon, QPixmap, QColor
from PyQt5.QtCore import Qt, QLocale, pyqtSlot from PyQt5.QtCore import Qt, QLocale, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@@ -39,6 +41,9 @@ from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog from novelwriter.extensions.pageddialog import NPagedDialog
from novelwriter.extensions.configlayout import NConfigLayout from novelwriter.extensions.configlayout import NConfigLayout
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -49,7 +54,7 @@ class GuiProjectSettings(NPagedDialog):
TAB_IMPORT = 2 TAB_IMPORT = 2
TAB_REPLACE = 3 TAB_REPLACE = 3
def __init__(self, mainGui, focusTab=TAB_MAIN): def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiProjectSettings") logger.debug("Create: GuiProjectSettings")
@@ -441,7 +446,7 @@ class GuiProjectEditStatus(QWidget):
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot delete a status item that is in use." "Cannot delete a status item that is in use."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
else: else:
self.listBox.takeTopLevelItem(iRow) self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
+2 -2
View File
@@ -128,13 +128,13 @@ class GuiWordList(QDialog):
if word == "": if word == "":
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot add a blank word." "Cannot add a blank word."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return return
if self.listBox.findItems(word, Qt.MatchExactly): if self.listBox.findItems(word, Qt.MatchExactly):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"The word '{0}' is already in the word list." "The word '{0}' is already in the word list."
).format(word), nwAlert.ERROR) ).format(word), level=nwAlert.ERROR)
return return
self.listBox.addItem(word) self.listBox.addItem(word)
+1 -1
View File
@@ -125,7 +125,7 @@ class nwAlert(Enum):
INFO = 0 INFO = 0
WARN = 1 WARN = 1
ERROR = 2 ERROR = 2
BUG = 3 ASK = 3
# END Enum nwAlert # END Enum nwAlert
+21 -17
View File
@@ -1,7 +1,6 @@
""" """
novelWriter Exception Handling novelWriter Exception Handling
================================ ================================
Error handling function and error dialog
File History: File History:
Created: 2020-08-02 [0.10.2] 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 You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import sys import sys
import random import random
import logging import logging
from typing import TYPE_CHECKING
from PyQt5.QtGui import QFont, QFontDatabase from PyQt5.QtGui import QFont, QFontDatabase
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel, QWidget, qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel,
QDialogButtonBox QDialogButtonBox
) )
if TYPE_CHECKING: # pragma: no cover
from types import TracebackType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,15 +46,15 @@ logger = logging.getLogger(__name__)
# Utility Functions # Utility Functions
# =============================================================================================== # # =============================================================================================== #
def logException(): def logException() -> None:
"""Log the content of an exception message. """Log the content of an exception message."""
"""
exType, exValue, _ = sys.exc_info() exType, exValue, _ = sys.exc_info()
if exType is not None: if exType is not None:
logger.error("%s: %s", exType.__name__, str(exValue)) 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 """Format an exception as a string the same way the default
exception handler does. exception handler does.
""" """
@@ -62,7 +67,7 @@ def formatException(exc):
class NWErrorMessage(QDialog): class NWErrorMessage(QDialog):
def __init__(self, parent): def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self.setObjectName("NWErrorMessage") self.setObjectName("NWErrorMessage")
@@ -113,7 +118,7 @@ class NWErrorMessage(QDialog):
return 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 """Generate a message and append session data, error info and
error traceback. error traceback.
""" """
@@ -142,7 +147,7 @@ class NWErrorMessage(QDialog):
enchantVersion = "Unknown" enchantVersion = "Unknown"
try: try:
exTrace = "\n".join(format_tb(exTrace)) txtTrace = "\n".join(format_tb(exTrace))
self.msgBody.setPlainText(( self.msgBody.setPlainText((
"Environment:\n" "Environment:\n"
f"novelWriter Version: {__version__}\n" f"novelWriter Version: {__version__}\n"
@@ -151,7 +156,7 @@ class NWErrorMessage(QDialog):
f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n"
f"enchant: {enchantVersion}\n\n" f"enchant: {enchantVersion}\n\n"
f"{exType.__name__}:\n{str(exValue)}\n\n" f"{exType.__name__}:\n{str(exValue)}\n\n"
f"Traceback:\n{exTrace}\n" f"Traceback:\n{txtTrace}\n"
)) ))
except Exception: except Exception:
self.msgBody.setPlainText("Failed to generate error report ...") self.msgBody.setPlainText("Failed to generate error report ...")
@@ -162,18 +167,17 @@ class NWErrorMessage(QDialog):
# Slots # Slots
## ##
def _doClose(self): @pyqtSlot()
"""Close the dialog. def _doClose(self) -> None:
""" """Close the dialog."""
self.close() self.close()
return return
# END Class NWErrorMessage # END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace): def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None:
"""Function to catch unhandled global exceptions. """Function to catch unhandled global exceptions."""
"""
from traceback import print_tb from traceback import print_tb
from PyQt5.QtWidgets import qApp from PyQt5.QtWidgets import qApp
+25 -30
View File
@@ -1,7 +1,6 @@
""" """
novelWriter GUI Document Editor novelWriter GUI Document Editor
================================= =================================
GUI classes for the main document editor
File History: File History:
Created: 2018-09-29 [0.0.1] GuiDocEditor Created: 2018-09-29 [0.0.1] GuiDocEditor
@@ -28,12 +27,14 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import bisect import bisect
import logging import logging
from enum import Enum from enum import Enum
from time import time from time import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression, Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression,
@@ -44,9 +45,8 @@ from PyQt5.QtGui import (
QPalette, QTextDocument, QCursor, QPixmap QPalette, QTextDocument, QCursor, QPixmap
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, QAction, qApp, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget
QFrame
) )
from novelwriter import CONFIG from novelwriter import CONFIG
@@ -57,6 +57,9 @@ from novelwriter.core.index import countWords
from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.core.spellcheck import NWSpellEnchant
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -75,7 +78,7 @@ class GuiDocEditor(QTextEdit):
novelStructureChanged = pyqtSignal() novelStructureChanged = pyqtSignal()
novelItemMetaChanged = pyqtSignal(str) novelItemMetaChanged = pyqtSignal(str)
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiDocEditor") logger.debug("Create: GuiDocEditor")
@@ -357,7 +360,7 @@ class GuiDocEditor(QTextEdit):
).format( ).format(
f"{docSize/1.0e6:.2f}", f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), nwAlert.ERROR) ), level=nwAlert.ERROR)
self.clearEditor() self.clearEditor()
return False return False
@@ -457,7 +460,7 @@ class GuiDocEditor(QTextEdit):
).format( ).format(
f"{docSize/1.0e6:.2f}", f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -496,20 +499,19 @@ class GuiDocEditor(QTextEdit):
if not self._nwDocument.writeDocument(docText): if not self._nwDocument.writeDocument(docText):
saveOk = False saveOk = False
if self._nwDocument._currHash != self._nwDocument._prevHash: if self._nwDocument._currHash != self._nwDocument._prevHash:
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(self.tr(
self.tr("File Changed on Disk"), "This document has been changed outside of novelWriter "
self.tr( "while it was open. Overwrite the file on disk?"
"This document has been changed outside of novelWriter " ))
"while it was open. Overwrite the file on disk?"
)
)
if msgYes: if msgYes:
saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) saveOk = self._nwDocument.writeDocument(docText, forceWrite=True)
if not saveOk: if not saveOk:
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("Could not save document."), self._nwDocument.getError() self.tr("Could not save document."),
], nwAlert.ERROR) info=self._nwDocument.getError(),
level=nwAlert.ERROR
)
return False return False
@@ -726,7 +728,7 @@ class GuiDocEditor(QTextEdit):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Spell checking requires the package PyEnchant. " "Spell checking requires the package PyEnchant. "
"It does not appear to be installed." "It does not appear to be installed."
), nwAlert.INFO) ))
theMode = False theMode = False
if self.spEnchant.spellLanguage is None: if self.spEnchant.spellLanguage is None:
@@ -868,17 +870,10 @@ class GuiDocEditor(QTextEdit):
if self._nwDocument is None: if self._nwDocument is None:
logger.error("No document open") logger.error("No document open")
return False return False
self.mainGui.makeAlert(
msgBox = QMessageBox() self.tr("The currently open file is saved in:"),
msgBox.information( info=self._nwDocument.getFileLocation()
self,
self.tr("File Location"),
"%s<br>%s" % (
self.tr("The currently open file is saved in:"),
self._nwDocument.getFileLocation()
),
) )
return return
def insertText(self, theInsert): def insertText(self, theInsert):
@@ -1116,7 +1111,7 @@ class GuiDocEditor(QTextEdit):
"The maximum size of a single novelWriter document is {0} MB." "The maximum size of a single novelWriter document is {0} MB."
).format( ).format(
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), nwAlert.ERROR) ), level=nwAlert.ERROR)
self.undo() self.undo()
return return
@@ -1675,7 +1670,7 @@ class GuiDocEditor(QTextEdit):
if not theCursor.hasSelection(): if not theCursor.hasSelection():
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Please select some text before calling replace quotes." "Please select some text before calling replace quotes."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
+23 -31
View File
@@ -589,7 +589,7 @@ class GuiProjectTree(QTreeWidget):
if sHandle is None or pItem is None: if sHandle is None or pItem is None:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!" "Did not find anywhere to add the file or folder!"
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
# Collect some information about the selected item # Collect some information about the selected item
@@ -600,7 +600,7 @@ class GuiProjectTree(QTreeWidget):
if self.theProject.tree.isTrash(sHandle): if self.theProject.tree.isTrash(sHandle):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder." "Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
# Set default label and determine if new item is to be added # Set default label and determine if new item is to be added
@@ -838,7 +838,7 @@ class GuiProjectTree(QTreeWidget):
if trashHandle is None: if trashHandle is None:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"There is currently no Trash folder in this project." "There is currently no Trash folder in this project."
), nwAlert.INFO) ))
return False return False
theTrash = self.getTreeFromHandle(trashHandle) theTrash = self.getTreeFromHandle(trashHandle)
@@ -849,11 +849,10 @@ class GuiProjectTree(QTreeWidget):
if nTrash == 0: if nTrash == 0:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"The Trash folder is already empty." "The Trash folder is already empty."
), nwAlert.INFO) ))
return False return False
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(
self.tr("Empty Trash"),
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
) )
if not msgYes: if not msgYes:
@@ -900,7 +899,6 @@ class GuiProjectTree(QTreeWidget):
if askFirst: if askFirst:
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
) )
if not msgYes: if not msgYes:
@@ -937,7 +935,7 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() > 0: if trItemS.childCount() > 0:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Root folders can only be deleted when they are empty." "Root folders can only be deleted when they are empty."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
logger.debug("Permanently deleting root folder '%s'", tHandle) logger.debug("Permanently deleting root folder '%s'", tHandle)
@@ -956,7 +954,6 @@ class GuiProjectTree(QTreeWidget):
else: else:
if askFirst: if askFirst:
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
) )
if not msgYes: if not msgYes:
@@ -1525,13 +1522,10 @@ class GuiProjectTree(QTreeWidget):
"""Convert a folder to a note or document.""" """Convert a folder to a note or document."""
tItem = self.theProject.tree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is not None and tItem.isFolderType(): if tItem is not None and tItem.isFolderType():
msgYes = self.mainGui.askQuestion( msgYes = self.mainGui.askQuestion(self.tr(
self.tr("Convert Folder"), "Do you want to convert the folder to a {0}? "
self.tr( "This action cannot be reversed."
"Do you want to convert the folder to a {0}? " ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
"This action cannot be reversed."
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))
)
if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
tItem.setType(nwItemType.FILE) tItem.setType(nwItemType.FILE)
tItem.setLayout(nwItemLayout.DOCUMENT) tItem.setLayout(nwItemLayout.DOCUMENT)
@@ -1570,9 +1564,7 @@ class GuiProjectTree(QTreeWidget):
mrgData = dlgMerge.getData() mrgData = dlgMerge.getData()
mrgList = mrgData.get("finalItems", []) mrgList = mrgData.get("finalItems", [])
if not mrgList: if not mrgList:
self.mainGui.makeAlert([ self.mainGui.makeAlert(self.tr("No documents selected for merging."))
self.tr("No documents selected for merging.")
], nwAlert.INFO)
return False return False
# Save the open document first, in case it's part of merge # Save the open document first, in case it's part of merge
@@ -1595,9 +1587,10 @@ class GuiProjectTree(QTreeWidget):
docMerger.appendText(sHandle, True, mLabel) docMerger.appendText(sHandle, True, mLabel)
if not docMerger.writeTargetDoc(): if not docMerger.writeTargetDoc():
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("Could not write document content."), docMerger.getError() self.tr("Could not write document content."),
], nwAlert.ERROR) info=docMerger.getError(), level=nwAlert.ERROR
)
return False return False
self.theProject.index.reIndexHandle(mHandle) self.theProject.index.reIndexHandle(mHandle)
@@ -1658,9 +1651,10 @@ class GuiProjectTree(QTreeWidget):
self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
self._alertTreeChange(dHandle, flush=False) self._alertTreeChange(dHandle, flush=False)
if not writeOk: if not writeOk:
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("Could not write document content."), docSplit.getError() self.tr("Could not write document content."),
], nwAlert.ERROR) info=docSplit.getError(), level=nwAlert.ERROR
)
if splitData.get("moveToTrash", False): if splitData.get("moveToTrash", False):
self.moveItemToTrash(tHandle, askFirst=False, flush=True) self.moveItemToTrash(tHandle, askFirst=False, flush=True)
@@ -1680,13 +1674,11 @@ class GuiProjectTree(QTreeWidget):
if nItems == 0: if nItems == 0:
return False return False
elif nItems == 1: elif nItems == 1:
qTitle = self.tr("Duplicate Document") question = self.tr("Do you want to duplicate this document?")
qText = self.tr("Do you want to duplicate this document?")
else: else:
qTitle = self.tr("Duplicate from Here") question = self.tr("Do you want to duplicate this item and all child items?")
qText = self.tr("Do you want to duplicate this item and all child items?")
if not self.mainGui.askQuestion(qTitle, qText): if not self.mainGui.askQuestion(question):
return False return False
docDup = DocDuplicator(self.theProject) docDup = DocDuplicator(self.theProject)
@@ -1698,7 +1690,7 @@ class GuiProjectTree(QTreeWidget):
dupCount += 1 dupCount += 1
if dupCount != nItems: if dupCount != nItems:
self.mainGui.makeAlert(self.tr("Could not duplicate all items."), nwAlert.WARN) self.mainGui.makeAlert(self.tr("Could not duplicate all items."), level=nwAlert.WARN)
self.saveTreeOrder() self.saveTreeOrder()
@@ -1757,7 +1749,7 @@ class GuiProjectTree(QTreeWidget):
else: else:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"There is nowhere to add item with name '{0}'." "There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), nwAlert.ERROR) ).format(nwItem.itemName), level=nwAlert.ERROR)
return None return None
byIndex = -1 byIndex = -1
+9 -8
View File
@@ -445,14 +445,15 @@ class GuiIcons:
ICON_KEYS = { ICON_KEYS = {
# Project and GUI Icons # Project and GUI Icons
"novelwriter", "build_excluded", "build_filtered", "build_included", "cls_archive", "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn",
"cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", "build_excluded", "build_filtered", "build_included", "cls_archive", "cls_character",
"cls_plot", "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", "cls_plot",
"proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", "proj_document",
"proj_stats", "proj_title", "search_cancel", "search_case", "search_loop", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", "proj_stats",
"search_preserve", "search_project", "search_regex", "search_word", "status_idle", "proj_title", "search_cancel", "search_case", "search_loop", "search_preserve",
"status_lang", "status_lines", "status_stats", "status_time", "view_build", "view_editor", "search_project", "search_regex", "search_word", "status_idle", "status_lang",
"view_novel", "view_outline", "status_lines", "status_stats", "status_time", "view_build", "view_editor", "view_novel",
"view_outline",
# General Button Icons # General Button Icons
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit", "add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
+88 -108
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot 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 ( from PyQt5.QtWidgets import (
qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter,
QStackedWidget, QVBoxLayout, QWidget QStackedWidget, QVBoxLayout, QWidget
@@ -66,7 +66,7 @@ from novelwriter.enum import (
nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
) )
from novelwriter.common import getGuiItem, hexToInt from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles, nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -112,9 +112,11 @@ class GuiMain(QMainWindow):
# Core Classes # Core Classes
# ============ # ============
# Core Classes and Settings # Core Classes
self.mainTheme = GuiTheme() self.mainTheme = GuiTheme()
self.theProject = NWProject(self) self.theProject = NWProject(self)
# Core Settings
self.hasProject = False self.hasProject = False
self.isFocusMode = False self.isFocusMode = False
self.idleRefTime = time() self.idleRefTime = time()
@@ -133,6 +135,7 @@ class GuiMain(QMainWindow):
# ============= # =============
# Sizes # Sizes
iPx = self.mainTheme.fontPixelSize
mPx = CONFIG.pxInt(4) mPx = CONFIG.pxInt(4)
hWd = CONFIG.pxInt(4) hWd = CONFIG.pxInt(4)
@@ -301,6 +304,15 @@ class GuiMain(QMainWindow):
# Forward Functions # Forward Functions
self.setStatus = self.mainStatus.setStatus self.setStatus = self.mainStatus.setStatus
# Cache Alert Pixmaps
pxSize = (2*iPx, 2*iPx)
self.alertPix: dict[nwAlert, QPixmap] = {
nwAlert.INFO: self.mainTheme.getPixmap("alert_info", pxSize),
nwAlert.WARN: self.mainTheme.getPixmap("alert_warn", pxSize),
nwAlert.ERROR: self.mainTheme.getPixmap("alert_error", pxSize),
nwAlert.ASK: self.mainTheme.getPixmap("alert_question", pxSize),
}
# Check that config loaded fine # Check that config loaded fine
self.reportConfErr() self.reportConfErr()
@@ -320,7 +332,7 @@ class GuiMain(QMainWindow):
"You are running an untested development version of novelWriter. " "You are running an untested development version of novelWriter. "
"Please be careful when working on a live project " "Please be careful when working on a live project "
"and make sure you take regular backups." "and make sure you take regular backups."
), nwAlert.WARN) ), level=nwAlert.WARN)
logger.info("novelWriter is ready ...") logger.info("novelWriter is ready ...")
self.setStatus(self.tr("novelWriter is ready ...")) self.setStatus(self.tr("novelWriter is ready ..."))
@@ -380,7 +392,7 @@ class GuiMain(QMainWindow):
if not self.closeProject(): if not self.closeProject():
self.makeAlert(self.tr( self.makeAlert(self.tr(
"Cannot create a new project when another project is open." "Cannot create a new project when another project is open."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
if projData is None: if projData is None:
@@ -398,7 +410,7 @@ class GuiMain(QMainWindow):
self.makeAlert(self.tr( self.makeAlert(self.tr(
"A project already exists in that location. " "A project already exists in that location. "
"Please choose another folder." "Please choose another folder."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
logger.info("Creating new project") logger.info("Creating new project")
@@ -420,13 +432,10 @@ class GuiMain(QMainWindow):
return True return True
if not isYes: if not isYes:
msgYes = self.askQuestion( msgYes = self.askQuestion("%s<br>%s" % (
self.tr("Close Project"), self.tr("Close the current project?"),
"%s<br>%s" % ( self.tr("Changes are saved automatically.")
self.tr("Close the current project?"), ))
self.tr("Changes are saved automatically.")
)
)
if not msgYes: if not msgYes:
return False return False
@@ -438,10 +447,7 @@ class GuiMain(QMainWindow):
if self.theProject.data.doBackup and CONFIG.backupOnClose: if self.theProject.data.doBackup and CONFIG.backupOnClose:
doBackup = True doBackup = True
if CONFIG.askBeforeBackup: if CONFIG.askBeforeBackup:
msgYes = self.askQuestion( msgYes = self.askQuestion(self.tr("Backup the current project?"))
self.tr("Backup Project"),
self.tr("Backup the current project?")
)
if not msgYes: if not msgYes:
doBackup = False doBackup = False
@@ -480,19 +486,28 @@ class GuiMain(QMainWindow):
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
# The project open failed. # The project open failed.
lockStatus = self.theProject.getLockStatus() lockStatus = self.theProject.getLockStatus()
if lockStatus is None: if lockStatus is None:
# The project is not locked, so failed for some other # The project is not locked, so failed for some other
# reason handled by the project class. # reason handled by the project class.
return False 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: try:
lockDetails = ( lockDetails = self.tr(
"<br>%s" % self.tr( "The project was locked by the computer "
"The project was locked by the computer " "'{0}' ({1} {2}), last active on {3}."
"'{0}' ({1} {2}), last active on {3}."
)
).format( ).format(
lockStatus[0], lockStatus[1], lockStatus[2], lockStatus[0], lockStatus[1], lockStatus[2],
datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X") datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X")
@@ -500,27 +515,7 @@ class GuiMain(QMainWindow):
except Exception: except Exception:
lockDetails = "" lockDetails = ""
msgBox = QMessageBox() if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN):
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 not self.theProject.openProject(projFile, overrideLock=True): if not self.theProject.openProject(projFile, overrideLock=True):
return False return False
else: else:
@@ -560,9 +555,7 @@ class GuiMain(QMainWindow):
# Check if we need to rebuild the index # Check if we need to rebuild the index
if self.theProject.index.indexBroken: if self.theProject.index.indexBroken:
self.makeAlert(self.tr( self.makeAlert(self.tr("The project index is outdated or broken. Rebuilding index."))
"The project index is outdated or broken. Rebuilding index."
), nwAlert.INFO)
self.rebuildIndex() self.rebuildIndex()
# Make sure the changed status is set to false on things opened # Make sure the changed status is set to false on things opened
@@ -751,23 +744,20 @@ class GuiMain(QMainWindow):
except Exception as exc: except Exception as exc:
self.makeAlert(self.tr( self.makeAlert(self.tr(
"Could not read file. The file must be an existing text file." "Could not read file. The file must be an existing text file."
), nwAlert.ERROR, exception=exc) ), level=nwAlert.ERROR, exception=exc)
return False return False
if self.docEditor.docHandle() is None: if self.docEditor.docHandle() is None:
self.makeAlert(self.tr( self.makeAlert(self.tr(
"Please open a document to import the text file into." "Please open a document to import the text file into."
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return False return False
if not self.docEditor.isEmpty(): if not self.docEditor.isEmpty():
msgYes = self.askQuestion( msgYes = self.askQuestion(self.tr(
self.tr("Import Document"), "Importing the file will overwrite the current content of "
self.tr( "the document. Do you want to proceed?"
"Importing the file will overwrite the current content of " ))
"the document. Do you want to proceed?"
)
)
if not msgYes: if not msgYes:
return False return False
@@ -866,9 +856,7 @@ class GuiMain(QMainWindow):
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
if not beQuiet: if not beQuiet:
self.makeAlert(self.tr( self.makeAlert(self.tr("The project index has been successfully rebuilt."))
"The project index has been successfully rebuilt."
), nwAlert.INFO)
return True return True
@@ -916,7 +904,7 @@ class GuiMain(QMainWindow):
if dlgConf.needsRestart: if dlgConf.needsRestart:
self.makeAlert(self.tr( self.makeAlert(self.tr(
"Some changes will not be applied until novelWriter has been restarted." "Some changes will not be applied until novelWriter has been restarted."
), nwAlert.INFO) ))
if dlgConf.refreshTree: if dlgConf.refreshTree:
self.projView.populateTree() self.projView.populateTree()
@@ -1097,53 +1085,48 @@ class GuiMain(QMainWindow):
return return
def makeAlert(self, message: list[str] | str, level: nwAlert = nwAlert.INFO, def makeAlert(self, text: str, info: str = "", details: str = "",
exception: Exception | None = None) -> None: level: nwAlert = nwAlert.INFO, exception: Exception | None = None) -> None:
"""Alert both the user and the logger at the same time. The """Alert both the user and the logger at the same time. The
message can be either a string or a list of strings. message can be either a string or a list of strings.
""" """
if isinstance(message, list): logText = " ".join(filter(None, [text, info, details]))
message = list(filter(None, message)) # Strip empty strings if level == nwAlert.INFO:
popMsg = "<br>".join(message) logger.info(logText, stacklevel=2)
logMsg = " ".join(message) elif level == nwAlert.WARN:
else: logger.warning(logText, stacklevel=2)
popMsg = str(message) elif level == nwAlert.ERROR:
logMsg = str(message) logger.error(logText, stacklevel=2, exc_info=exception)
kw = {}
if exception is not None: if exception is not None:
kw["exc_info"] = exception excText = f"{type(exception).__name__}: {str(exception)}"
popMsg = f"{popMsg}<br>{type(exception).__name__}: {str(exception)}" info = f"{info}<br>{excText}" if info else excText
# Write to Log msgBox = QMessageBox(self)
if level == nwAlert.INFO: msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level]))
logger.info(logMsg, **kw) msgBox.setText(text)
elif level == nwAlert.WARN: msgBox.setInformativeText(info)
logger.warning(logMsg, **kw) msgBox.setDetailedText(details)
elif level == nwAlert.ERROR: msgBox.setStandardButtons(QMessageBox.Ok)
logger.error(logMsg, **kw) msgBox.setIconPixmap(self.alertPix[level])
elif level == nwAlert.BUG: msgBox.adjustSize()
logger.error(logMsg, **kw) msgBox.exec_()
# 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 += "<br>%s" % self.tr("This is a bug!")
msgBox.critical(self, self.tr("Internal Error"), popMsg)
return 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.""" """Ask the user a Yes/No question, and return the answer."""
msgBox = QMessageBox() msgBox = QMessageBox(self)
msgRes = msgBox.question(self, title, question, QMessageBox.Yes | QMessageBox.No) msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level]))
return msgRes == QMessageBox.Yes 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: def reportConfErr(self) -> bool:
"""Checks if the Config module has any errors to report, and let """Checks if the Config module has any errors to report, and let
@@ -1151,7 +1134,7 @@ class GuiMain(QMainWindow):
errors since it is initialised before the GUI itself. errors since it is initialised before the GUI itself.
""" """
if CONFIG.hasError: if CONFIG.hasError:
self.makeAlert(CONFIG.errorText(), nwAlert.ERROR) self.makeAlert(CONFIG.errorText(), level=nwAlert.ERROR)
return True return True
return False return False
@@ -1162,13 +1145,10 @@ class GuiMain(QMainWindow):
def closeMain(self) -> bool: def closeMain(self) -> bool:
"""Save everything, and close novelWriter.""" """Save everything, and close novelWriter."""
if self.hasProject: if self.hasProject:
msgYes = self.askQuestion( msgYes = self.askQuestion("%s<br>%s" % (
self.tr("Exit"), self.tr("Do you want to exit novelWriter?"),
"%s<br>%s" % ( self.tr("Changes are saved automatically.")
self.tr("Do you want to exit novelWriter?"), ))
self.tr("Changes are saved automatically.")
)
)
if not msgYes: if not msgYes:
return False return False
@@ -1419,7 +1399,7 @@ class GuiMain(QMainWindow):
"from the Tools menu, or by pressing {1}." "from the Tools menu, or by pressing {1}."
).format( ).format(
tag, "F9" tag, "F9"
), nwAlert.ERROR) ), level=nwAlert.ERROR)
return None, None return None, None
return tHandle, sTitle return tHandle, sTitle
+1 -2
View File
@@ -310,7 +310,7 @@ class GuiManuscriptBuild(QDialog):
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
bPath = Path(self.buildPath.text()) bPath = Path(self.buildPath.text())
if not bPath.is_dir(): if not bPath.is_dir():
self.mainGui.makeAlert(self.tr("Output folder does not exist."), nwAlert.ERROR) self.mainGui.makeAlert(self.tr("Output folder does not exist."), level=nwAlert.ERROR)
return False return False
bExt = nwLabels.BUILD_EXT[bFormat] bExt = nwLabels.BUILD_EXT[bFormat]
@@ -318,7 +318,6 @@ class GuiManuscriptBuild(QDialog):
if buildPath.exists(): if buildPath.exists():
if not self.mainGui.askQuestion( if not self.mainGui.askQuestion(
self.tr("File Exists"),
self.tr("The file already exists. Do you want to overwrite it?") self.tr("The file already exists. Do you want to overwrite it?")
): ):
return False return False
+1 -5
View File
@@ -270,11 +270,7 @@ class GuiManuscript(QDialog):
"""Delete the currently selected build settings entry.""" """Delete the currently selected build settings entry."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if build is not None: if build is not None:
proceed = self.mainGui.askQuestion( if self.mainGui.askQuestion(self.tr("Delete build '{0}'?".format(build.name))):
self.tr("Delete Build"),
self.tr("Delete build '{0}'?".format(build.name))
)
if proceed:
self._builds.removeBuild(build.buildID) self._builds.removeBuild(build.buildID)
self._updateBuildsList() self._updateBuildsList()
return return
+6 -7
View File
@@ -244,15 +244,14 @@ class GuiBuildSettings(QDialog):
## ##
def _askToSaveBuild(self) -> None: def _askToSaveBuild(self) -> None:
"""Check if there are unsaved changes, and if there are, ask if """Check if there are unsaved changes, and if there are, ask
it's ok to reject them. whether the user wants to save them.
""" """
if self._build.changed: if self._build.changed:
doSave = self.mainGui.askQuestion( response = self.mainGui.askQuestion(self.tr(
self.tr("Build Settings"), "Do you want to save your changes to '{0}'?".format(self._build.name)
self.tr("Do you want to save your changes?") ))
) if response:
if doSave:
self._emitBuildData() self._emitBuildData()
self._build.resetChangedState() self._build.resetChangedState()
return return
+8 -6
View File
@@ -415,13 +415,15 @@ class GuiWritingStats(QDialog):
# Report to user # Report to user
if wSuccess: if wSuccess:
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("{0} file successfully written to:").format(textFmt), savePath self.tr("{0} file successfully written to:").format(textFmt),
], nwAlert.INFO) info=savePath
)
else: else:
self.mainGui.makeAlert([ self.mainGui.makeAlert(
self.tr("Failed to write {0} file.").format(textFmt), errMsg self.tr("Failed to write {0} file.").format(textFmt),
], nwAlert.ERROR) info=errMsg, level=nwAlert.ERROR
)
return wSuccess return wSuccess
+2 -4
View File
@@ -145,10 +145,8 @@ def mockGUI():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def nwGUI(qtbot, monkeypatch, functionFixture): def nwGUI(qtbot, monkeypatch, functionFixture):
"""Create an instance of the novelWriter GUI.""" """Create an instance of the novelWriter GUI."""
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
+8 -8
View File
@@ -39,22 +39,22 @@ class MockGuiMain(QObject):
# Test Variables # Test Variables
self.askResponse = True self.askResponse = True
self.lastAlert = "" self.lastAlert = ""
self.lastQuestion = ("", "") self.lastQuestion = ""
return return
def postLaunchTasks(self, cmdOpen): def postLaunchTasks(self, cmdOpen):
return return
def makeAlert(self, message, level=0, exception=None): def makeAlert(self, text, info="", detals="", level=0, exception=None):
assert isinstance(message, str) or isinstance(message, list) assert isinstance(text, str)
print("%s: %s" % (str(level), message)) print("%s: %s" % (str(level), text))
self.lastAlert = str(message) self.lastAlert = str(text)
return return
def askQuestion(self, title, qustion): def askQuestion(self, text, info="", details="", level=3):
print("Question: %s" % qustion) print("Question: %s" % text)
self.lastQuestion = (title, qustion) self.lastQuestion = text
return self.askResponse return self.askResponse
def setStatus(self, theMessage): def setStatus(self, theMessage):
+3 -3
View File
@@ -224,7 +224,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mockGUI.askResponse = False mockGUI.askResponse = False
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1] assert "The file format of your project is about to be" in mockGUI.lastQuestion
mockGUI.askResponse = True mockGUI.askResponse = True
# Won't open project from newer version # Won't open project from newer version
@@ -232,7 +232,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mockGUI.askResponse = False mockGUI.askResponse = False
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1] assert "This project was saved by a newer version" in mockGUI.lastQuestion
mockGUI.askResponse = True mockGUI.askResponse = True
# Fail checking items should still pass # Fail checking items should still pass
@@ -249,7 +249,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mockGUI.askResponse = True mockGUI.askResponse = True
theProject.index._indexBroken = True theProject.index._indexBroken = True
assert theProject.openProject(fncPath) is True assert theProject.openProject(fncPath) is True
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1] assert "The file format of your project is about to be" in mockGUI.lastQuestion
assert theProject.index._indexBroken is False assert theProject.index._indexBroken is False
theProject.closeProject() theProject.closeProject()
+1 -1
View File
@@ -114,7 +114,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
# Close project # Close project
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
nwGUI.hasProject = True nwGUI.hasProject = True
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert nwGUI.newProject(projData={"projPath": projPath}) is False assert nwGUI.newProject(projData={"projPath": projPath}) is False
# No project path # No project path
+3 -6
View File
@@ -34,12 +34,9 @@ LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW)
@pytest.mark.skipif(not LANG_DATA, reason="No i18n Data") @pytest.mark.skipif(not LANG_DATA, reason="No i18n Data")
@pytest.mark.parametrize("language", [a for a, b in LANG_DATA]) @pytest.mark.parametrize("language", [a for a, b in LANG_DATA])
def testI18n_Localisation(qtbot, monkeypatch, language, fncPath): def testI18n_Localisation(qtbot, monkeypatch, language, fncPath):
"""test loading the gui with a specific language. """Test loading the gui with a specific language."""
""" monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# Set the test langauge # Set the test langauge
CONFIG.guiLocale = language CONFIG.guiLocale = language
+5 -5
View File
@@ -642,7 +642,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
# The document isn't empty, so the message box should pop # The document isn't empty, so the message box should pop
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a, **k: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.No)
assert not nwGUI.importDocument() assert not nwGUI.importDocument()
assert nwGUI.docEditor.getText() == "Bar" assert nwGUI.docEditor.getText() == "Bar"
@@ -655,16 +655,16 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
theMessage = "" theMessage = ""
def recordMsg(*args): def recordMsg(*args, **kwargs):
nonlocal theMessage nonlocal theMessage
theMessage = args[3] theMessage = "%s|%s" % (args[0], kwargs["info"])
return None return None
assert not theMessage assert not theMessage
monkeypatch.setattr(QMessageBox, "information", recordMsg) monkeypatch.setattr(nwGUI, "makeAlert", recordMsg)
nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger)
theBits = theMessage.split("<br>") theBits = theMessage.split("|")
assert len(theBits) == 2 assert len(theBits) == 2
assert theBits[0] == "The currently open file is saved in:" assert theBits[0] == "The currently open file is saved in:"
assert theBits[1] == str(projPath / "content" / "000000000000f.nwd") assert theBits[1] == str(projPath / "content" / "000000000000f.nwd")
+5 -5
View File
@@ -397,7 +397,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath,
# User cancels action # User cancels action
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree.moveItemToTrash(C.hTitlePage) is False assert projTree.moveItemToTrash(C.hTitlePage) is False
assert theProject.tree.isTrash(C.hTitlePage) is False assert theProject.tree.isTrash(C.hTitlePage) is False
@@ -444,7 +444,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro
# User cancels action # User cancels action
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree.permDeleteItem(C.hTitlePage) is False assert projTree.permDeleteItem(C.hTitlePage) is False
assert C.hTitlePage in theProject.tree assert C.hTitlePage in theProject.tree
@@ -496,7 +496,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock
# User cancels # User cancels
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree.emptyTrash() is False assert projTree.emptyTrash() is False
assert C.hTitlePage in theProject.tree assert C.hTitlePage in theProject.tree
assert C.hChapterDir in theProject.tree assert C.hChapterDir in theProject.tree
@@ -617,7 +617,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Click no on the dialog # Click no on the dialog
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert nwGUI.theProject.tree[hNewFolderOne].isFolderType() assert nwGUI.theProject.tree[hNewFolderOne].isFolderType()
@@ -864,7 +864,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock
# Duplicate title page, but select no # Duplicate title page, but select no
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree._duplicateFromHandle(C.hTitlePage) is False assert projTree._duplicateFromHandle(C.hTitlePage) is False
assert len(nwGUI.theProject.tree) == 8 assert len(nwGUI.theProject.tree) == 8
+1 -1
View File
@@ -129,7 +129,7 @@ def testManuscriptBuild_Main(
manus.buildPath.setText(str(fncPath)) manus.buildPath.setText(str(fncPath))
manus.buildName.setText("TestBuild") manus.buildName.setText("TestBuild")
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert manus._runBuild() is False assert manus._runBuild() is False
# Finish # Finish