diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 3dfecf24..57f9432e 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -24,6 +24,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from __future__ import annotations
+
import os
import json
import shutil
@@ -46,7 +48,7 @@ from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.common import (
- checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, hexToInt, isHandle,
+ checkBool, checkInt, checkStringNone, formatTimeStamp, hexToInt, isHandle,
makeFileNameSafe, minmax, simplified,
)
@@ -122,7 +124,7 @@ class NWProject(QObject):
@property
def projChanged(self):
- return self._projChanged or self._data.changed
+ return self._projChanged
@property
def projAltered(self):
@@ -596,7 +598,9 @@ class NWProject(QObject):
content = self._projTree.pack()
xmlWriter = ProjectXMLWriter(self.projPath)
if not xmlWriter.write(self._data, content, saveTime, editTime):
- self.mainGui.makeAlert(self.tr("Failed to save project."), nwAlert.ERROR)
+ self.mainGui.makeAlert(self.tr(
+ "Failed to save project."
+ ), nwAlert.ERROR, exception=xmlWriter.error)
return False
# Save project GUI options
@@ -1331,17 +1335,18 @@ class NWProjectData:
self._language = None
self._spellCheck = False
self._spellLang = None
- self._lastHandle = {
- "editor": "",
- "viewer": "",
- "novelTree": "",
- "outline": "",
- }
- self._lastCount = {}
- self._currCount = {}
- self._autoReplace = {}
- self._titleFormat = {
+ # Project Dictionaries
+ self._lastCount: dict[str, int] = {}
+ self._currCount: dict[str, int] = {}
+ self._lastHandle: dict[str, str | None] = {
+ "editor": None,
+ "viewer": None,
+ "novelTree": None,
+ "outline": None,
+ }
+ self._autoReplace: dict[str, str] = {}
+ self._titleFormat: dict[str, str] = {
"title": "%title%",
"chapter": "%title%",
"unnumbered": "%title%",
@@ -1562,12 +1567,12 @@ class NWProjectData:
values.
"""
if isinstance(component, str):
- self._lastHandle[component] = checkString(value, "")
+ self._lastHandle[component] = checkStringNone(value, None)
self.theProject.setProjectChanged(True)
elif isinstance(value, dict):
for key, entry in value.items():
if key in self._lastHandle:
- self._lastHandle[key] = checkString(entry, "")
+ self._lastHandle[key] = str(entry) if isHandle(entry) else None
self.theProject.setProjectChanged(True)
return
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index e16cca0a..1c3f525d 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -266,11 +266,7 @@ class ProjectXMLReader:
projData.setSpellCheck(xItem.text)
elif xItem.tag == "spellLang":
projData.setSpellLang(xItem.text)
- elif xItem.tag == "lastEdited": # Discontinued in 1.4
- projData.setLastHandle(xItem.text, "editor")
- elif xItem.tag == "lastViewed": # Discontinued in 1.4
- projData.setLastHandle(xItem.text, "viewer")
- elif xItem.tag == "lastWordCount":
+ elif xItem.tag == "totalWordCount":
projData.setLastCount(xItem.text, "total")
elif xItem.tag == "novelWordCount":
projData.setLastCount(xItem.text, "novel")
@@ -281,16 +277,30 @@ class ProjectXMLReader:
elif xItem.tag in ("import", "importance"):
self._parseStatusImport(xItem, projData.itemImport)
elif xItem.tag == "lastHandle":
- projData.setLastHandle(self._parseDictKeyText(xItem, "component"))
+ projData.setLastHandle(self._parseDictKeyText(xItem))
elif xItem.tag == "autoReplace":
if self._version >= 0x0102:
- projData.setAutoReplace(self._parseDictKeyText(xItem, "key"))
+ projData.setAutoReplace(self._parseDictKeyText(xItem))
else: # Pre 1.2 format
projData.setAutoReplace(self._parseDictTagText(xItem))
elif xItem.tag == "titleFormat":
- projData.setTitleFormat(self._parseDictTagText(xItem))
+ if self._version >= 0x0104:
+ projData.setTitleFormat(self._parseDictKeyText(xItem))
+ else: # Pre 1.4 format
+ projData.setTitleFormat(self._parseDictTagText(xItem))
else:
- logger.warning("Ignored in xml", xItem.tag)
+ if self._version < 0x0104:
+ # Convert some deprecated fields
+ if xItem.tag == "lastEdited": # Discontinued in 1.4
+ projData.setLastHandle(xItem.text, "editor")
+ elif xItem.tag == "lastViewed": # Discontinued in 1.4
+ projData.setLastHandle(xItem.text, "viewer")
+ elif xItem.tag == "lastWordCount": # Renamed in 1.4
+ projData.setLastCount(xItem.text, "total")
+ else:
+ logger.warning("Ignored in xml", xItem.tag)
+ else:
+ logger.warning("Ignored in xml", xItem.tag)
return True
@@ -419,14 +429,14 @@ class ProjectXMLReader:
self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()}
return
- def _parseDictKeyText(self, xItem, keyName):
+ def _parseDictKeyText(self, xItem):
"""Parse a dictionary stored with key as an attribute and the
value as the text porperty.
"""
result = {}
for xEntry in xItem:
- if xEntry.tag == "entry" and keyName in xEntry.attrib:
- result[xEntry.attrib[keyName]] = checkString(xEntry.text, "")
+ if xEntry.tag == "entry" and "key" in xEntry.attrib:
+ result[xEntry.attrib["key"]] = checkString(xEntry.text, "")
return result
def _parseDictTagText(self, xItem):
@@ -447,7 +457,21 @@ class ProjectXMLWriter:
return
+ ##
+ # Properties
+ ##
+
+ @property
+ def error(self):
+ return self._error
+
+ ##
+ # Methods
+ ##
+
def write(self, projData, projContent, saveTime, editTime):
+ """Write the project data and content to the XML files.
+ """
nwXML = etree.Element("novelWriterXML", attrib={
"appVersion": str(novelwriter.__version__),
@@ -471,27 +495,24 @@ class ProjectXMLWriter:
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellCheck", projData.spellCheck)
self._packSingleValue(xSettings, "spellLang", projData.spellLang)
- self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total"))
+ self._packSingleValue(xSettings, "totalWordCount", projData.getCurrCount("total"))
self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel"))
self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes"))
- self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle, "component")
- self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace, "key")
- self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat)
+ self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle)
+ self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
+ self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
# Save Status/Importance
xStatus = etree.SubElement(xSettings, "status")
- for (label, attr) in projData.itemStatus.pack():
- xEntry = etree.SubElement(xStatus, "entry", attrib=attr)
- xEntry.text = label
+ for label, attrib in projData.itemStatus.pack():
+ self._packSingleValue(xStatus, "entry", label, attrib=attrib)
xImport = etree.SubElement(xSettings, "importance")
- for (label, attr) in projData.itemImport.pack():
- xEntry = etree.SubElement(xImport, "entry", attrib=attr)
- xEntry.text = label
+ for label, attrib in projData.itemImport.pack():
+ self._packSingleValue(xImport, "entry", label, attrib=attrib)
# Save Tree Content
- cAttr = {"count": str(len(projContent))}
- xContent = etree.SubElement(nwXML, "content", attrib=cAttr)
+ xContent = etree.SubElement(nwXML, "content", attrib={"count": str(len(projContent))})
for item in projContent:
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
@@ -499,8 +520,8 @@ class ProjectXMLWriter:
xName.text = item["name"]
# Write the xml tree to file
- tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
+ tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
@@ -510,7 +531,8 @@ class ProjectXMLWriter:
encoding="utf-8",
xml_declaration=True
))
- except Exception:
+ except Exception as exc:
+ self._error = exc
return False
# If we're here, the file was successfully saved,
@@ -519,7 +541,8 @@ class ProjectXMLWriter:
if os.path.isfile(saveFile):
os.replace(saveFile, backFile)
os.replace(tempFile, saveFile)
- except OSError:
+ except OSError as exc:
+ self._error = exc
return False
return True
@@ -528,40 +551,29 @@ class ProjectXMLWriter:
# Internal Functions
##
- def _packSingleValue(self, xParent, name, value, allowNone=True):
- """Pack a list of values into an xml element.
+ def _packSingleValue(self, xParent, name, value, attrib=None):
+ """Pack a single value into an xml element.
"""
- if (value == "" or value is None) and not allowNone:
- return
- xItem = etree.SubElement(xParent, name)
- xItem.text = str(value)
+ xItem = etree.SubElement(xParent, name, attrib=attrib)
+ xItem.text = str(value) or ""
return
- def _packListValue(self, xParent, name, data, allowNone=True):
+ def _packListValue(self, xParent, name, data):
"""Pack a list of values into an xml element.
"""
for value in data:
- self._packSingleValue(xParent, name, value, allowNone=allowNone)
+ xItem = etree.SubElement(xParent, name)
+ xItem.text = str(value) or ""
return
- def _packDictKeyValue(self, xParent, name, data, keyName):
+ def _packDictKeyValue(self, xParent, name, data):
"""Pack the entries of a dictionary into an xml element.
"""
xItem = etree.SubElement(xParent, name)
for key, value in data.items():
if len(key) > 0:
- xEntry = etree.SubElement(xItem, "entry", attrib={keyName: key})
- if value:
- xEntry.text = value
- return
-
- def _packDictTagValue(self, xParent, name, data):
- """Pack the entries of a dictionary into an xml element.
- """
- xItem = etree.SubElement(xParent, name)
- for aKey, value in data.items():
- if len(aKey) > 0:
- self._packSingleValue(xItem, aKey, value)
+ xEntry = etree.SubElement(xItem, "entry", attrib={"key": key})
+ xEntry.text = str(value) or ""
return
# END Class ProjectXMLWriter
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index dca0b5a5..d6eeee62 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,27 +1,27 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1403
+ 1407
236
- 69454
+ 69424
False
en_GB
True
None
- 1363
+ 1363
954
409
- 636b6aa9b697b
- 636b6aa9b697b
- 7031beac91f75
- 7031beac91f75
+ 636b6aa9b697b
+ 636b6aa9b697b
+ 7031beac91f75
+ 7031beac91f75
B
@@ -29,11 +29,11 @@
D
- %title%
- Chapter %chw%: %title%
- %title%
- Scene %ch%.%sc%: %title%
-
+ %title%
+ Chapter %chw%: %title%
+ %title%
+ Scene %ch%.%sc%: %title%
+
New
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index bcc0b0ce..5ac1b4de 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,35 +1,37 @@
-
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
- 28
+ 32
24
- 1874
+ 1889
False
en_GB
False
None
- 7a992350f3eb6
- None
- None
- None
- 3847
+ 3847
3109
738
+
+ 7a992350f3eb6
+ None
+ b3643d0f92e32
+ None
+
Replace Text 1
Replace Text 2
- %title%
- Chapter %ch%: %title%
- %title%
- * * *
-
+ %title%
+ Chapter %ch%: %title%
+ %title%
+ * * *
+
New
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx
index 3b9d81c6..19689fac 100644
--- a/tests/minimal/nwProject.nwx
+++ b/tests/minimal/nwProject.nwx
@@ -1,33 +1,35 @@
-
+
Test Minimal
Minimal
Jane Doe
John Doh
- 21
+ 25
2
- 177
+ 203
True
en_GB
False
None
- None
- None
- a508bb932959c
- None
- 10
+ 10
10
0
+
+ None
+ None
+ a508bb932959c
+ None
+
- %title%
- Chapter %ch%: %title%
- %title%
- * * *
-
+ %title%
+ Chapter %ch%: %title%
+ %title%
+ * * *
+
New
diff --git a/tests/mock.py b/tests/mock.py
index ff67f4bf..74623dd1 100644
--- a/tests/mock.py
+++ b/tests/mock.py
@@ -19,14 +19,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from PyQt5.QtCore import QObject
+
# =========================================================================== #
# Mock GUI
# =========================================================================== #
-class MockGuiMain:
+class MockGuiMain(QObject):
def __init__(self):
+ super().__init__()
+
self.mainConf = None
self.hasProject = True
self.theProject = None
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx
index a42b8290..69987568 100644
--- a/tests/reference/coreProject_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -14,20 +14,22 @@
None
False
None
- None
- None
- None
- None
- 0
+ 0
0
0
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx
index 63deaff4..a4a5e280 100644
--- a/tests/reference/coreProject_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -14,20 +14,22 @@
None
False
None
- None
- None
- None
- None
- 0
+ 0
0
0
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 8a390542..e3d19827 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -13,20 +13,22 @@
None
False
None
- None
- None
- None
- None
- 13
+ 13
10
3
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx
index 2bc604a6..7fcc9b61 100644
--- a/tests/reference/coreProject_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
None
@@ -12,20 +12,22 @@
None
False
None
- None
- None
- None
- None
- 0
+ 0
0
0
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 1614eb63..d256a9a9 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -13,20 +13,22 @@
None
False
None
- None
- None
- None
- None
- 9
+ 9
9
0
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index ad38f479..69f7208c 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,32 +1,34 @@
-
+
New Project
New Novel
Jane Doe
4
2
- 3
+ 4
True
None
True
None
- 000000000000f
- None
- 0000000000008
- 0000000000008
- 163
+ 163
136
27
+
+ 000000000000f
+ None
+ 0000000000008
+ 0000000000008
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 2e36ef35..9dfb7b59 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
New Novel
@@ -13,20 +13,22 @@
None
False
None
- None
- None
- None
- None
- 9
+ 9
9
0
+
+ None
+ None
+ None
+ None
+
- %title%
- %title%
- %title%
- * * *
-
+ %title%
+ %title%
+ %title%
+ * * *
+
New