diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 1058628b..ce1a1278 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -75,7 +75,7 @@ class NWIndex: return def __repr__(self): - return f"" + return f"" ## # Properties diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 76294a08..50fe0cef 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -38,9 +38,10 @@ from PyQt5.QtCore import QCoreApplication from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( - checkString, checkStringNone, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, minmax, simplified + checkString, checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe, + hexToInt, minmax, simplified ) +from novelwriter.constants import trConst, nwFiles, nwLabels from novelwriter.core.tree import NWTree from novelwriter.core.item import NWItem from novelwriter.core.index import NWIndex @@ -48,7 +49,7 @@ from novelwriter.core.status import NWStatus from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState -from novelwriter.constants import trConst, nwFiles, nwLabels +from novelwriter.core.projectdata import NWProjectData logger = logging.getLogger(__name__) @@ -63,7 +64,8 @@ class NWProject: self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self._data = {} + self._data = NWProjectData() + self._raw = {} # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -91,7 +93,6 @@ class NWProject: self.projFiles = [] # A list of all files in the content folder on load # Project Meta - self.projName = "" # Project name self.bookTitle = "" # The final title; should only be used for exports self.bookAuthors = [] # A list of book authors @@ -125,6 +126,10 @@ class NWProject: # Properties ## + @property + def data(self): + return self._data + @property def index(self): return self._projIndex @@ -263,7 +268,6 @@ class NWProject: self.projSpell = None self.projLang = None self.projFiles = [] - self.projName = "" self.bookTitle = "" self.bookAuthors = [] self.autoReplace = {} @@ -328,14 +332,14 @@ class NWProject: if not self.setProjectPath(projPath, newProject=True): return False - self.setProjectName(projName) + self.data.setName(projName) self.setBookTitle(projTitle) self.setBookAuthors(projAuthors) hNovelRoot = self.newRoot(nwItemClass.NOVEL) hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot) - titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName) + titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self._data.name) if self.bookAuthors: titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) @@ -480,8 +484,9 @@ class NWProject: # Open The Project XML File # ========================= + self._data = NWProjectData() xmlReader = ProjectXMLReader(fileName) - xmlParsed = xmlReader.read() + xmlParsed = xmlReader.read(self._data) xmlData = xmlReader.data print(json.dumps(xmlData, indent=2)) @@ -510,7 +515,7 @@ class NWProject: self.clearProject() return False - self._data = xmlData + self._raw = xmlData logger.debug("XML root is '%s'", nwxRoot) logger.debug("File version is '%s'", xmlVersion) @@ -552,16 +557,13 @@ class NWProject: # Extract Data # ============ - xmlProject = xmlData.get("project", {}) + self.bookTitle = self._data.title + self.bookAuthors = self._data.autors + self.saveCount = self._data.saveCount + self.autoCount = self._data.autoCount + self.editTime = self._data.editTime - self.projName = xmlProject.get("name", "") - self.bookTitle = xmlProject.get("title", "") - self.bookAuthors = xmlProject.get("authors", []) - self.saveCount = xmlProject.get("saveCount", 0) - self.autoCount = xmlProject.get("autoCount", 0) - self.editTime = xmlProject.get("editTime", 0) - - logger.info("Project Name: '%s'", self.projName) + logger.info("Project Name: '%s'", self._data.name) logger.info("Project Title: '%s'", self.bookTitle) xmlSettings = xmlData.get("settings", {}) @@ -601,7 +603,7 @@ class NWProject: self._deprecatedFiles() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) + self.mainConf.updateRecentCache(self.projPath, self._data.name, self.lastWCount, time()) self.mainConf.saveRecentCache() # Check the project tree consistency @@ -621,7 +623,7 @@ class NWProject: self._writeLockFile() self.setProjectChanged(False) - self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name)) return True @@ -662,7 +664,7 @@ class NWProject: # Save Project Meta xProject = etree.SubElement(nwXML, "project") - self._packProjectValue(xProject, "name", self.projName) + self._packProjectValue(xProject, "name", self._data.name) self._packProjectValue(xProject, "title", self.bookTitle) self._packProjectValue(xProject, "author", self.bookAuthors) self._packProjectValue(xProject, "saveCount", str(self.saveCount)) @@ -734,11 +736,11 @@ class NWProject: self._optState.saveSettings() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime) + self.mainConf.updateRecentCache(self.projPath, self._data.name, self.currWCount, saveTime) self.mainConf.saveRecentCache() self._writeLockFile() - self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) return True @@ -800,14 +802,14 @@ class NWProject: ), nwAlert.ERROR) return False - if not self.projName: + if not self._data.name: self.mainGui.makeAlert(self.tr( "Cannot backup project because no project name is set. " "Please set a Working Title in Project Settings." ), nwAlert.ERROR) return False - cleanName = makeFileNameSafe(self.projName) + cleanName = makeFileNameSafe(self._data.name) baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName)) if not os.path.isdir(baseDir): try: @@ -953,14 +955,6 @@ class NWProject: return True - def setProjectName(self, projName): - """Set the project name, This is the the name used for backup - files etc. - """ - self.projName = simplified(projName) - self.setProjectChanged(True) - return True - def setBookTitle(self, bookTitle): """Set the book title, that is, the title to include in exports. """ @@ -998,7 +992,7 @@ class NWProject: ), nwAlert.WARN) return False - if self.projName == "": + if self._data.name == "": self.mainGui.makeAlert(self.tr( "You must set a valid project name in Project Settings to " "use the automatic project backup feature." diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py new file mode 100644 index 00000000..f5719949 --- /dev/null +++ b/novelwriter/core/projectdata.py @@ -0,0 +1,112 @@ +""" +novelWriter – Project Data Class +================================ +Class for holding the project settings + +File History: +Created: 2022-10-30 [2.0rc1] + +This file is a part of novelWriter +Copyright 2018–2022, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import logging + +from novelwriter.common import checkInt, simplified + +logger = logging.getLogger(__name__) + + +class NWProjectData: + + def __init__(self): + + # Project Meta + self._name = "" + self._title = "" + self._authors = [] + self._saveCount = 0 + self._autoCount = 0 + self._editTime = 0 + + # Internal + self._changed = False + + return + + ## + # Properties + ## + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def autors(self): + return self._authors + + @property + def saveCount(self): + return self._saveCount + + @property + def autoCount(self): + return self._autoCount + + @property + def editTime(self): + return self._editTime + + ## + # Setters + ## + + def setName(self, value): + self._name = simplified(str(value)) + self._changed = True + return + + def setTitle(self, value): + self._title = simplified(str(value)) + self._changed = True + return + + def addAuthor(self, value): + self._authors.append(simplified(str(value))) + self._changed = True + return + + def setSaveCount(self, value): + self._saveCount = checkInt(value, 0) + self._changed = True + return + + def setAutoCount(self, value): + self._autoCount = checkInt(value, 0) + self._changed = True + return + + def setEditTime(self, value): + self._editTime = checkInt(value, 0) + self._changed = True + return + +# END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 2aa2d248..7b78dd2c 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -4,7 +4,8 @@ novelWriter – Project XML Read/Write Classes for reading and writing the project XML file File History: -Created: 2022-09-28 [1.7.b1] +Created: 2022-09-28 [2.0rc1] ProjectXMLReader +Created: 2022-09-28 [2.0rc1] XMLReadState This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -90,7 +91,7 @@ class ProjectXMLReader: # Methods ## - def read(self): + def read(self, projData): """Read and parse the project XML file. """ self._data = {} @@ -154,7 +155,7 @@ class ProjectXMLReader: status = True for xSection in xRoot: if xSection.tag == "project": - status &= self._parseProjectMeta(xSection) + status &= self._parseProjectMeta(xSection, projData) elif xSection.tag == "settings": status &= self._parseProjectSettings(xSection) elif xSection.tag == "content": @@ -180,31 +181,25 @@ class ProjectXMLReader: # Internal Functions ## - def _parseProjectMeta(self, xSection): + def _parseProjectMeta(self, xSection, projData): """Parse the project section of the XML file. """ logger.debug("Parsing xml ") - data = {} - authors = [] for xItem in xSection: if xItem.tag == "name": - data["name"] = simplified(checkString(xItem.text, "")) + projData.setName(xItem.text) elif xItem.tag == "title": - data["title"] = simplified(checkString(xItem.text, "")) + projData.setTitle(xItem.text) elif xItem.tag == "author": - authors.append(simplified(checkString(xItem.text, ""))) + projData.addAuthor(xItem.text) elif xItem.tag == "saveCount": - data["saveCount"] = checkInt(xItem.text, 0) + projData.setSaveCount(xItem.text) elif xItem.tag == "autoCount": - data["autoCount"] = checkInt(xItem.text, 0) + projData.setAutoCount(xItem.text) elif xItem.tag == "editTime": - data["editTime"] = checkInt(xItem.text, 0) + projData.setEditTime(xItem.text) else: logger.warning("Ignored in xml", xItem.tag) - - data["authors"] = authors - self._data["project"] = data - return True def _parseProjectSettings(self, xSection): diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 2110300c..88647d63 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -315,7 +315,7 @@ class ToHtml(Tokenizer): "\n" "\n" ).format( - projTitle=self.theProject.projName, + projTitle=self.theProject.data.name, htmlStyle="\n".join(theStyle), bodyText=bodyText, ) diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 7f2259c3..368c25ee 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -166,7 +166,7 @@ class GuiProjectDetailsMain(QWidget): self.bookTitle.setWordWrap(True) self.projName = QLabel( - self.tr("Working Title: {0}").format(self.theProject.projName) + self.tr("Working Title: {0}").format(self.theProject.data.name) ) workFont = self.projName.font() workFont.setPointSizeF(0.8*fPt) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index eb96ffb2..78165613 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -114,7 +114,7 @@ class GuiProjectSettings(PagedDialog): spellLang = self.tabMain.spellLang.currentData() doBackup = not self.tabMain.doBackup.isChecked() - self.theProject.setProjectName(projName) + self.theProject.data.setName(projName) self.theProject.setBookTitle(bookTitle) self.theProject.setBookAuthors(bookAuthors) self.theProject.setProjBackup(doBackup) @@ -209,7 +209,7 @@ class GuiProjectEditMain(QWidget): self.editName = QLineEdit() self.editName.setMaxLength(200) self.editName.setMaximumWidth(xW) - self.editName.setText(self.theProject.projName) + self.editName.setText(self.theProject.data.name) self.mainForm.addRow( self.tr("Project name"), self.editName, diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 89e4ca44..af76edf8 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -383,7 +383,7 @@ class GuiMain(QMainWindow): self.mainStatus.setDocumentStatus(nwState.NONE) self.mainStatus.setStatus(self.tr("New project created ...")) - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) else: self.theProject.clearProject() @@ -521,7 +521,7 @@ class GuiMain(QMainWindow): self.theProject.index.loadIndex() # Update GUI - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) self.rebuildTrees() self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) @@ -960,7 +960,7 @@ class GuiMain(QMainWindow): if dlgProj.spellChanged: self.docEditor.setDictionaries() self.itemDetails.refreshDetails() - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) return True diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 683f8e99..3529d577 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -888,7 +888,7 @@ class GuiBuildNovel(QDialog): # Generate File Name # ================== - cleanName = makeFileNameSafe(self.theProject.projName) + cleanName = makeFileNameSafe(self.theProject.data.name) fileName = "%s.%s" % (cleanName, fileExt) saveDir = self.mainConf.lastPath if not os.path.isdir(saveDir): @@ -972,7 +972,7 @@ class GuiBuildNovel(QDialog): elif theFmt == self.FMT_JSON_H or theFmt == self.FMT_JSON_M: jsonData = { "meta": { - "workingTitle": self.theProject.projName, + "workingTitle": self.theProject.data.name, "novelTitle": self.theProject.bookTitle, "authors": self.theProject.bookAuthors, "buildTime": self.buildTime, diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 5a95295b..62d1f830 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -202,7 +202,7 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir): assert theProject.newProject(projData) is True assert theProject.openProject(fncDir) is True - assert theProject.projName == "Sample Project" + assert theProject.data.name == "Sample Project" assert theProject.saveProject() is True assert theProject.closeProject() is True os.unlink(dstSample) @@ -236,7 +236,7 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir): monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx") assert theProject.newProject(projData) is True assert theProject.openProject(fncDir) is True - assert theProject.projName == "Sample Project" + assert theProject.data.name == "Sample Project" assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -895,8 +895,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectPath(fncDir) # Project Name - assert theProject.setProjectName(" A Name ") - assert theProject.projName == "A Name" + assert theProject.data.setName(" A Name ") + assert theProject.data.name == "A Name" # Project Title assert theProject.setBookTitle(" A Title ") @@ -944,9 +944,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): theProject.mainConf.backupPath = tmpDir assert theProject.setProjBackup(True) - assert theProject.setProjectName("") + assert theProject.data.setName("") assert not theProject.setProjBackup(True) - assert theProject.setProjectName("A Name") + assert theProject.data.setName("A Name") assert theProject.setProjBackup(True) # Spell check @@ -1327,12 +1327,12 @@ def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): # Missing project name theProject.mainConf.backupPath = tmpDir - theProject.projName = "" + theProject.data.name = "" assert theProject.zipIt(doNotify=False) is False # Non-existent folder theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent") - theProject.projName = "Test Minimal" + theProject.data.name = "Test Minimal" assert theProject.zipIt(doNotify=False) is False # Same folder as project (causes infinite loop in zipping) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index bb38f317..7e8b4e1d 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -136,7 +136,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd assert projSettings.spellChanged is False projSettings._doSave() - assert theProject.projName == "Project Name" + assert theProject.data.name == "Project Name" assert theProject.bookTitle == "Project Title" assert theProject.bookAuthors == ["Jane Doe", "John Doh"] diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 79fcbe52..497f521b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -172,7 +172,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None - assert nwGUI.theProject.projName == "" + assert nwGUI.theProject.data.name == "" assert nwGUI.theProject.bookTitle == "" assert len(nwGUI.theProject.bookAuthors) == 0 assert not nwGUI.theProject.spellCheck @@ -194,7 +194,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath == fncProj assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") - assert nwGUI.theProject.projName == "New Project" + assert nwGUI.theProject.data.name == "New Project" assert nwGUI.theProject.bookTitle == "New Novel" assert len(nwGUI.theProject.bookAuthors) == 1 assert nwGUI.theProject.spellCheck is False diff --git a/tests/tools.py b/tests/tools.py index 4a97c930..bf8d2c40 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -167,7 +167,7 @@ def buildTestProject(theObject, projPath): theProject.clearProject() theProject.setProjectPath(projPath, newProject=True) - theProject.setProjectName("New Project") + theProject.data.setName("New Project") theProject.setBookTitle("New Novel") theProject.setBookAuthors("Jane Doe")