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