From 13b9e5a43487ad7bf2b46600e70404c6898a1a49 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 21:25:40 +0100 Subject: [PATCH] Move project data class to project file --- novelwriter/core/project.py | 330 ++++++++++++++++++++++++++++++-- novelwriter/core/projectdata.py | 271 -------------------------- novelwriter/core/projectxml.py | 50 +++-- novelwriter/gui/noveltree.py | 10 +- novelwriter/guimain.py | 2 + sample/nwProject.nwx | 16 +- 6 files changed, 353 insertions(+), 326 deletions(-) delete mode 100644 novelwriter/core/projectdata.py diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c0230758..3dfecf24 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -4,7 +4,8 @@ novelWriter – Project Wrapper Data class for novelWriter projects File History: -Created: 2018-09-29 [0.0.1] +Created: 2018-09-29 [0.0.1] NWProject +Created: 2022-10-30 [2.0rc1] NWProjectData This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -32,38 +33,40 @@ import novelwriter from time import time from functools import partial -from PyQt5.QtCore import QCoreApplication +from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException -from novelwriter.common import ( - 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 +from novelwriter.core.status import NWStatus from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState -from novelwriter.core.projectdata import NWProjectData +from novelwriter.common import ( + checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, hexToInt, isHandle, + makeFileNameSafe, minmax, simplified, +) + logger = logging.getLogger(__name__) -class NWProject: +class NWProject(QObject): - FILE_VERSION = "1.4" # The current project file format version + projectStatusChanged = pyqtSignal(bool) def __init__(self, mainGui): + super().__init__(parent=mainGui) # Internal self.mainConf = novelwriter.CONFIG self.mainGui = mainGui # Project Data - self._data = NWProjectData() + self._data = NWProjectData(self) # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -240,7 +243,7 @@ class NWProject: # Project Tree self._projTree.clear() - self._data = NWProjectData() + self._data = NWProjectData(self) # Project Settings self.projPath = None @@ -446,7 +449,7 @@ class NWProject: # Open The Project XML File # ========================= - self._data = NWProjectData() + self._data = NWProjectData(self) xmlReader = ProjectXMLReader(fileName) xmlParsed = xmlReader.read(self._data) @@ -857,13 +860,10 @@ class NWProject: information to the GUI statusbar. """ self._projChanged = bValue - self.mainGui.mainStatus.doUpdateProjectStatus(bValue) + self.projectStatusChanged.emit(self._projChanged) if bValue is True: # If we've changed the project at all, this should be True self._projAltered = True - else: - # If we're resetting the status, also reset for data class - self._data.resetProjectChanged() return self._projChanged ## @@ -1310,3 +1310,301 @@ class NWProject: return True # END Class NWProject + + +class NWProjectData: + + def __init__(self, theProject): + + self.theProject = theProject + + # Project Meta + self._name = "" + self._title = "" + self._authors = [] + self._saveCount = 0 + self._autoCount = 0 + self._editTime = 0 + + # Project Settings + self._doBackup = True + self._language = None + self._spellCheck = False + self._spellLang = None + self._lastHandle = { + "editor": "", + "viewer": "", + "novelTree": "", + "outline": "", + } + self._lastCount = {} + self._currCount = {} + + self._autoReplace = {} + self._titleFormat = { + "title": "%title%", + "chapter": "%title%", + "unnumbered": "%title%", + "scene": "* * *", + "section": "", + } + + self._status = NWStatus(NWStatus.STATUS) + self._import = NWStatus(NWStatus.IMPORT) + + return + + ## + # Properties + ## + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def authors(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 + + @property + def doBackup(self): + return self._doBackup + + @property + def language(self): + return self._language + + @property + def spellCheck(self): + return self._spellCheck + + @property + def spellLang(self): + return self._spellLang + + @property + def lastHandle(self): + return self._lastHandle + + @property + def autoReplace(self): + return self._autoReplace + + @property + def titleFormat(self): + return self._titleFormat + + @property + def itemStatus(self): + return self._status + + @property + def itemImport(self): + return self._import + + ## + # Methods + ## + + def addAuthor(self, value): + """Add an author to the authors list. + """ + self._authors.append(simplified(str(value))) + self.theProject.setProjectChanged(True) + return + + def incSaveCount(self): + """Increment the save count by one. + """ + self._saveCount += 1 + self.theProject.setProjectChanged(True) + return + + def incAutoCount(self): + """Increment the auto save count by one. + """ + self._autoCount += 1 + self.theProject.setProjectChanged(True) + return + + ## + # Getters + ## + + def getLastHandle(self, component): + """Retrieve the last used handle for a given component. + """ + return self._lastHandle.get(component, None) + + def getLastCount(self, type): + """Retrieve the last word count for a given type. + """ + return self._lastCount.get(type, 0) + + def getCurrCount(self, type): + """Retrieve the current word count for a given type. + """ + return self._currCount.get(type, 0) + + def getTitleFormat(self, kind): + """Retrieve the title format string for a given kind of header. + """ + return self._titleFormat.get(kind, "%title%") + + ## + # Setters + ## + + def setName(self, value): + """Set a new project name. + """ + if value != self._name: + self._name = simplified(str(value)) + self.theProject.setProjectChanged(True) + return + + def setTitle(self, value): + """Set a new novel title. + """ + if value != self._title: + self._title = simplified(str(value)) + self.theProject.setProjectChanged(True) + return + + def setAuthors(self, value): + """Set the list of authors from either a string with one author + per line, or a list of authors. + """ + self._authors = [] + self.theProject.setProjectChanged(True) + if isinstance(value, str): + for author in value.splitlines(): + author = simplified(author) + if author: + self._authors.append(author) + self.theProject.setProjectChanged(True) + elif isinstance(value, list): + self._authors = value + return + + def setSaveCount(self, value): + """Set the save count from last session. + """ + self._saveCount = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setAutoCount(self, value): + """Set the auto save count from last session. + """ + self._autoCount = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setEditTime(self, value): + """Set tyje edit time from last session. + """ + self._editTime = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setDoBackup(self, value): + """Set the do write backup flag. + """ + if value != self._doBackup: + self._doBackup = checkBool(value, False) + self.theProject.setProjectChanged(True) + return + + def setLanguage(self, value): + """Set the project language. + """ + if value != self._language: + self._language = checkStringNone(value, None) + self.theProject.setProjectChanged(True) + return + + def setSpellCheck(self, value): + """Set the spell check flag. + """ + if value != self._spellCheck: + self._spellCheck = checkBool(value, False) + self.theProject.setProjectChanged(True) + return + + def setSpellLang(self, value): + """Set the spell check language. + """ + if value != self._spellLang: + self._spellLang = checkStringNone(value, None) + self.theProject.setProjectChanged(True) + return + + def setLastHandle(self, value, component=None): + """Set a last used handle into the handle registry. If component + is None, the value is assumed to be the whole dictionary of + values. + """ + if isinstance(component, str): + self._lastHandle[component] = checkString(value, "") + 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.theProject.setProjectChanged(True) + return + + def setLastCount(self, value, type): + """Set the word counts from last session. + """ + self._lastCount[type] = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setCurrCount(self, value, type): + """Set the current word counts. + """ + if value != self._currCount.get(type, 0): + self._currCount[type] = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setAutoReplace(self, value): + """Set the auto-replace dictionary. + """ + if isinstance(value, dict): + self._autoReplace = {} + for key, entry in value.items(): + if isinstance(entry, str): + self._autoReplace[key] = simplified(entry) + self.theProject.setProjectChanged(True) + return + + def setTitleFormat(self, value): + """Set the title formats. + """ + if isinstance(value, dict): + for key, entry in value.items(): + if key in self._titleFormat and isinstance(entry, str): + self._titleFormat[key] = simplified(entry) + self.theProject.setProjectChanged(True) + return + +# END Class NWProjectData diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py deleted file mode 100644 index d91c39c7..00000000 --- a/novelwriter/core/projectdata.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -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 ( - checkBool, checkInt, checkStringNone, simplified -) -from novelwriter.core.status import NWStatus - -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 - - # Project Settings - self._doBackup = True - self._language = None - self._spellCheck = False - self._spellLang = None - self._lastHandle = {} - self._lastCount = {} - self._currCount = {} - - self._autoReplace = {} - self._titleFormat = { - "title": "%title%", - "chapter": "%title%", - "unnumbered": "%title%", - "scene": "* * *", - "section": "", - } - - self._status = NWStatus(NWStatus.STATUS) - self._import = NWStatus(NWStatus.IMPORT) - - # Internal - self._changed = False - - return - - ## - # Properties - ## - - @property - def name(self): - return self._name - - @property - def title(self): - return self._title - - @property - def authors(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 - - @property - def doBackup(self): - return self._doBackup - - @property - def language(self): - return self._language - - @property - def spellCheck(self): - return self._spellCheck - - @property - def spellLang(self): - return self._spellLang - - @property - def autoReplace(self): - return self._autoReplace - - @property - def titleFormat(self): - return self._titleFormat - - @property - def itemStatus(self): - return self._status - - @property - def itemImport(self): - return self._import - - @property - def changed(self): - return self._changed - - ## - # Methods - ## - - def addAuthor(self, value): - self._authors.append(simplified(str(value))) - self._changed = True - return - - def incSaveCount(self): - self._saveCount += 1 - self._changed = True - return - - def incAutoCount(self): - self._autoCount += 1 - self._changed = True - return - - def resetProjectChanged(self): - self._changed = False - - ## - # Getters - ## - - def getLastHandle(self, component): - return self._lastHandle.get(component, None) - - def getLastCount(self, type): - return self._lastCount.get(type, 0) - - def getCurrCount(self, type): - return self._currCount.get(type, 0) - - def getTitleFormat(self, kind): - return self._titleFormat.get(kind, "%title%") - - ## - # 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 setAuthors(self, value): - self._authors = [] - self._changed = True - if isinstance(value, str): - for author in value.splitlines(): - author = simplified(author) - if author: - self._authors.append(author) - self._changed = True - elif isinstance(value, list): - self._authors = value - 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 - - def setDoBackup(self, value): - self._doBackup = checkBool(value, False) - self._changed = True - return - - def setLanguage(self, value): - self._language = checkStringNone(value, None) - self._changed = True - return - - def setSpellCheck(self, value): - self._spellCheck = checkBool(value, False) - self._changed = True - return - - def setSpellLang(self, value): - self._spellLang = checkStringNone(value, None) - self._changed = True - return - - def setLastHandle(self, value, component): - self._lastHandle[component] = checkStringNone(value, None) - self._changed = True - return - - def setLastCount(self, value, type): - self._lastCount[type] = checkInt(value, 0) - self._changed = True - return - - def setCurrCount(self, value, type): - if value != self._currCount.get(type, 0): - self._currCount[type] = checkInt(value, 0) - self._changed = True - return - - def setAutoReplace(self, value): - if isinstance(value, dict): - self._autoReplace = {} - for key, entry in value.items(): - if isinstance(entry, str): - self._autoReplace[key] = simplified(entry) - self._changed = True - return - - def setTitleFormat(self, value): - if isinstance(value, dict): - for key, entry in value.items(): - if key in self._titleFormat and isinstance(entry, str): - self._titleFormat[key] = simplified(entry) - self._changed = True - return - -# END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 9b56a5d3..e16cca0a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -266,14 +266,10 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "lastEdited": + elif xItem.tag == "lastEdited": # Discontinued in 1.4 projData.setLastHandle(xItem.text, "editor") - elif xItem.tag == "lastViewed": + elif xItem.tag == "lastViewed": # Discontinued in 1.4 projData.setLastHandle(xItem.text, "viewer") - elif xItem.tag == "lastNovel": - projData.setLastHandle(xItem.text, "noveltree") - elif xItem.tag == "lastOutline": - projData.setLastHandle(xItem.text, "outline") elif xItem.tag == "lastWordCount": projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": @@ -284,9 +280,11 @@ class ProjectXMLReader: self._parseStatusImport(xItem, projData.itemStatus) elif xItem.tag in ("import", "importance"): self._parseStatusImport(xItem, projData.itemImport) + elif xItem.tag == "lastHandle": + projData.setLastHandle(self._parseDictKeyText(xItem, "component")) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - projData.setAutoReplace(self._parseDictKeyText(xItem)) + projData.setAutoReplace(self._parseDictKeyText(xItem, "key")) else: # Pre 1.2 format projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": @@ -304,13 +302,13 @@ class ProjectXMLReader: for xItem in xSection: if xItem.tag == "item": item = {} - item["handle"] = xItem.attrib.get("handle", None) - item["parent"] = xItem.attrib.get("parent", None) - item["root"] = xItem.attrib.get("root", None) + item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None) + item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None) + item["root"] = checkStringNone(xItem.attrib.get("root", None), None) item["order"] = checkInt(xItem.attrib.get("order", 0), 0) - item["type"] = checkString(xItem.attrib.get("type", ""), "") - item["class"] = checkString(xItem.attrib.get("class", ""), "") - item["layout"] = checkString(xItem.attrib.get("layout", ""), "") + item["type"] = checkString(xItem.attrib.get("type", "NO_TYPE"), "NO_TYPE") + item["class"] = checkString(xItem.attrib.get("class", "NO_CLASS"), "NO_CLASS") + item["layout"] = checkString(xItem.attrib.get("layout", "NO_LAYOUT"), "NO_LAYOUT") for xVal in xItem: if xVal.tag == "meta": item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False) @@ -339,7 +337,7 @@ class ProjectXMLReader: return True def _parseProjectContentLegacy(self, xSection): - """Parse the content section of the XML file for older version. + """Parse the content section of the XML file for older versions. """ logger.debug("Parsing xml (legacy format)") depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") @@ -347,8 +345,8 @@ class ProjectXMLReader: for xItem in xSection: item = {} if xItem.tag == "item": - item["handle"] = xItem.attrib.get("handle", None) - item["parent"] = xItem.attrib.get("parent", None) + item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None) + item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None) item["root"] = None # Value was added in 1.4 item["order"] = checkInt(xItem.attrib.get("order", 0), 0) item["heading"] = "H0" # Value was added in 1.4 @@ -421,14 +419,14 @@ class ProjectXMLReader: self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} return - def _parseDictKeyText(self, xItem): + def _parseDictKeyText(self, xItem, keyName): """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 "key" in xEntry.attrib: - result[xEntry.attrib["key"]] = checkString(xEntry.text, "") + if xEntry.tag == "entry" and keyName in xEntry.attrib: + result[xEntry.attrib[keyName]] = checkString(xEntry.text, "") return result def _parseDictTagText(self, xItem): @@ -473,14 +471,11 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "lastEdited", projData.getLastHandle("editor")) - self._packSingleValue(xSettings, "lastViewed", projData.getLastHandle("viewer")) - self._packSingleValue(xSettings, "lastNovel", projData.getLastHandle("noveltree")) - self._packSingleValue(xSettings, "lastOutline", projData.getLastHandle("outline")) self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total")) self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) - self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) + self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle, "component") + self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace, "key") self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat) # Save Status/Importance @@ -549,14 +544,15 @@ class ProjectXMLWriter: self._packSingleValue(xParent, name, value, allowNone=allowNone) return - def _packDictKeyValue(self, xParent, name, data): + def _packDictKeyValue(self, xParent, name, data, keyName): """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={"key": key}) - xEntry.text = value + xEntry = etree.SubElement(xItem, "entry", attrib={keyName: key}) + if value: + xEntry.text = value return def _packDictTagValue(self, xParent, name, data): diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index bfa1da1c..64662e17 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -109,7 +109,7 @@ class GuiNovelView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("noveltree")) + self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree")) return def clearProject(self): @@ -122,7 +122,7 @@ class GuiNovelView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastNovel = self.theProject.data.getLastHandle("noveltree") + lastNovel = self.theProject.data.getLastHandle("novelTree") if lastNovel not in self.theProject.tree: lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) @@ -319,7 +319,7 @@ class GuiNovelToolBar(QWidget): def _refreshNovelTree(self): """Rebuild the current tree. """ - rootHandle = self.theProject.data.getLastHandle("noveltree") + rootHandle = self.theProject.data.getLastHandle("novelTree") self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return @@ -485,7 +485,7 @@ class GuiNovelTree(QTreeWidget): titleKey = selItem[0].data(self.C_TITLE, self.D_KEY) self._populateTree(rootHandle) - self.theProject.data.setLastHandle(rootHandle, "noveltree") + self.theProject.data.setLastHandle(rootHandle, "novelTree") if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -523,7 +523,7 @@ class GuiNovelTree(QTreeWidget): self._lastCol = colType self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) if doRefresh: - lastNovel = self.theProject.data.getLastHandle("noveltree") + lastNovel = self.theProject.data.getLastHandle("novelTree") self.refreshTree(rootHandle=lastNovel, overRide=True) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 553667f6..9f776210 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -197,6 +197,8 @@ class GuiMain(QMainWindow): # Connect Signals # =============== + self.theProject.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) + self.viewsBar.viewChangeRequested.connect(self._changeView) self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 6b8d929d..dca0b5a5 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,26 +1,28 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1387 + 1403 236 - 69358 + 69454 False en_GB True None - 636b6aa9b697b - 636b6aa9b697b - 7031beac91f75 - 7031beac91f75 1363 954 409 + + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 + B E