From fa8d3ff816f41b312e7e271d0ff78af9b0b0470c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:14:13 +0200 Subject: [PATCH 1/5] Moved NWItem to core/item.py --- nw/core/item.py | 277 +++++++++++++++++++++++++++++++++++++++++++++ nw/core/project.py | 249 +--------------------------------------- 2 files changed, 278 insertions(+), 248 deletions(-) create mode 100644 nw/core/item.py diff --git a/nw/core/item.py b/nw/core/item.py new file mode 100644 index 00000000..5c0f26e0 --- /dev/null +++ b/nw/core/item.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Item Class + + novelWriter – Project Item Class +================================== + Class holding the data off a project tree item + + File History: + Created: 2018-10-27 [0.0.1] NWItem + + This file is a part of novelWriter + Copyright 2020, 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 +import nw + +from lxml import etree + +from nw.common import checkInt +from nw.constants import nwItemType, nwItemClass, nwItemLayout + +logger = logging.getLogger(__name__) + +class NWItem(): + + def __init__(self, theProject): + + self.theProject = theProject + + self.itemName = "" + self.itemHandle = None + self.parHandle = None + self.itemOrder = None + self.itemType = nwItemType.NO_TYPE + self.itemClass = nwItemClass.NO_CLASS + self.itemLayout = nwItemLayout.NO_LAYOUT + self.itemStatus = None + self.isExpanded = False + self.isExported = True + + # Document Meta Data + self.charCount = 0 + self.wordCount = 0 + self.paraCount = 0 + self.cursorPos = 0 + + return + + ## + # XML Pack/Unpack + ## + + def packXML(self, xParent): + """Packs all the data in the class instance into an XML object. + """ + xPack = etree.SubElement(xParent,"item",attrib={ + "handle" : str(self.itemHandle), + "order" : str(self.itemOrder), + "parent" : str(self.parHandle), + }) + xSub = self._subPack(xPack,"name", text=str(self.itemName)) + xSub = self._subPack(xPack,"type", text=str(self.itemType.name)) + xSub = self._subPack(xPack,"class", text=str(self.itemClass.name)) + xSub = self._subPack(xPack,"status", text=str(self.itemStatus)) + if self.itemType == nwItemType.FILE: + xSub = self._subPack(xPack,"exported", text=str(self.isExported)) + xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) + xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False) + xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False) + xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False) + xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False) + else: + xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) + return + + def unpackXML(self, xItem): + """Sets the values from an XML entry of type 'item'. + """ + if xItem.tag != "item": + logger.error("XML entry is not an NWItem") + return False + + if "handle" in xItem.attrib: + self.itemHandle = xItem.attrib["handle"] + else: + logger.error("XML item entry does not have a handle") + return False + + if "parent" in xItem.attrib: + self.parHandle = xItem.attrib["parent"] + + setMap = { + "name" : self.setName, + "order" : self.setOrder, + "type" : self.setType, + "class" : self.setClass, + "layout" : self.setLayout, + "status" : self.setStatus, + "expanded" : self.setExpanded, + "exported" : self.setExported, + "charCount" : self.setCharCount, + "wordCount" : self.setWordCount, + "paraCount" : self.setParaCount, + "cursorPos" : self.setCursorPos, + } + for xValue in xItem: + if xValue.tag in setMap: + setMap[xValue.tag](xValue.text) + else: + logger.error("Unknown tag '%s'" % xValue.tag) + + return True + + @staticmethod + def _subPack(xParent, name, attrib=None, text=None, none=True): + """Packs the values into an xml element. + """ + if not none and (text == None or text == "None"): + return None + xSub = etree.SubElement(xParent, name, attrib=attrib) + if text is not None: + xSub.text = text + return xSub + + ## + # Set Item Values + ## + + def setName(self, theName): + """Set the item name. + """ + self.itemName = theName.strip() + return + + def setHandle(self, theHandle): + """Set the item handle, and ensure it is valid. + """ + if isinstance(theHandle, str): + if len(theHandle) == 13: + self.itemHandle = theHandle + else: + self.itemHandle = None + else: + self.itemHandle = None + return + + def setParent(self, theParent): + """Set the parent handle, and ensure that it is valid. + """ + if theParent is None: + self.parHandle = None + elif isinstance(theParent, str): + if len(theParent) == 13: + self.parHandle = theParent + else: + self.parHandle = None + else: + self.parHandle = None + return + + def setOrder(self, theOrder): + """Set the item order, and ensure that it is valid. This value + is purely a meta value, not actually used by novelWriter. + """ + self.itemOrder = checkInt(theOrder, 0) + return + + def setType(self, theType): + """Set the item type from either a proper nwItemType, or set it + from a string representing a nwItemType. + """ + if isinstance(theType, nwItemType): + self.itemType = theType + elif theType in nwItemType.__members__: + self.itemType = nwItemType[theType] + else: + logger.error("Unrecognised item type '%s'" % theType) + self.itemType = nwItemType.NO_TYPE + return + + def setClass(self, theClass): + """Set the item class from either a proper nwItemClass, or set + it from a string representing a nwItemClass. + """ + if isinstance(theClass, nwItemClass): + self.itemClass = theClass + elif theClass in nwItemClass.__members__: + self.itemClass = nwItemClass[theClass] + else: + logger.error("Unrecognised item class '%s'" % theClass) + self.itemClass = nwItemClass.NO_CLASS + return + + def setLayout(self, theLayout): + """Set the item layout from either a proper nwItemLayout, or set + it from a string representing a nwItemLayout. + """ + if isinstance(theLayout, nwItemLayout): + self.itemLayout = theLayout + elif theLayout in nwItemLayout.__members__: + self.itemLayout = nwItemLayout[theLayout] + else: + logger.error("Unrecognised item layout '%s'" % theLayout) + self.itemLayout = nwItemLayout.NO_LAYOUT + return + + def setStatus(self, theStatus): + """Set the item status by looking it up in the valid status + items of the current project. + """ + if self.itemClass == nwItemClass.NOVEL: + self.itemStatus = self.theProject.statusItems.checkEntry(theStatus) + else: + self.itemStatus = self.theProject.importItems.checkEntry(theStatus) + return + + def setExpanded(self, expState): + """Save the expanded status of an item in the project tree. + """ + if isinstance(expState, str): + self.isExpanded = expState == str(True) + else: + self.isExpanded = expState == True + return + + def setExported(self, expState): + """Save the export flag. + """ + if isinstance(expState, str): + self.isExported = expState == str(True) + else: + self.isExported = expState == True + return + + ## + # Set Document Meta Data + ## + + def setCharCount(self, theCount): + """Set the character count, and ensure that it is an integer. + """ + self.charCount = checkInt(theCount, 0) + return + + def setWordCount(self, theCount): + """Set the word count, and ensure that it is an integer. + """ + self.wordCount = checkInt(theCount, 0) + return + + def setParaCount(self, theCount): + """Set the paragraph count, and ensure that it is an integer. + """ + self.paraCount = checkInt(theCount, 0) + return + + def setCursorPos(self, thePosition): + """Set the cursor position, and ensure that it is an integer. + """ + self.cursorPos = checkInt(thePosition, 0) + return + +# END Class NWItem diff --git a/nw/core/project.py b/nw/core/project.py index f7e412db..f6564012 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -7,10 +7,8 @@ File History: Created: 2018-09-29 [0.0.1] NWProject - Created: 2018-10-27 [0.0.1] NWItem Created: 2019-05-19 [0.1.3] NWStatus Created: 2019-10-21 [0.3.1] OptionState - Merged: 2020-05-07 [0.4.5] Moved NWItem class to this file Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file Added: 2020-05-07 [0.4.5] NWTree Rewritten: 2020-02-19 [0.4.5] OptionState @@ -44,6 +42,7 @@ from shutil import make_archive from PyQt5.QtWidgets import QMessageBox +from nw.core.item import NWItem from nw.core.document import NWDoc from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( @@ -1608,252 +1607,6 @@ class NWTree(): # END Class NWTree -# =============================================================================================== # -# NWItem -# Class holding the project items making up the NWProject -# =============================================================================================== # - -class NWItem(): - - def __init__(self, theProject): - - self.theProject = theProject - - self.itemName = "" - self.itemHandle = None - self.parHandle = None - self.itemOrder = None - self.itemType = nwItemType.NO_TYPE - self.itemClass = nwItemClass.NO_CLASS - self.itemLayout = nwItemLayout.NO_LAYOUT - self.itemStatus = None - self.isExpanded = False - self.isExported = True - - # Document Meta Data - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 - self.cursorPos = 0 - - return - - ## - # XML Pack/Unpack - ## - - def packXML(self, xParent): - """Packs all the data in the class instance into an XML object. - """ - xPack = etree.SubElement(xParent,"item",attrib={ - "handle" : str(self.itemHandle), - "order" : str(self.itemOrder), - "parent" : str(self.parHandle), - }) - xSub = self._subPack(xPack,"name", text=str(self.itemName)) - xSub = self._subPack(xPack,"type", text=str(self.itemType.name)) - xSub = self._subPack(xPack,"class", text=str(self.itemClass.name)) - xSub = self._subPack(xPack,"status", text=str(self.itemStatus)) - if self.itemType == nwItemType.FILE: - xSub = self._subPack(xPack,"exported", text=str(self.isExported)) - xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) - xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False) - xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False) - xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False) - xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False) - else: - xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) - return - - def unpackXML(self, xItem): - """Sets the values from an XML entry of type 'item'. - """ - if xItem.tag != "item": - logger.error("XML entry is not an NWItem") - return False - - if "handle" in xItem.attrib: - self.itemHandle = xItem.attrib["handle"] - else: - logger.error("XML item entry does not have a handle") - return False - - if "parent" in xItem.attrib: - self.parHandle = xItem.attrib["parent"] - - setMap = { - "name" : self.setName, - "order" : self.setOrder, - "type" : self.setType, - "class" : self.setClass, - "layout" : self.setLayout, - "status" : self.setStatus, - "expanded" : self.setExpanded, - "exported" : self.setExported, - "charCount" : self.setCharCount, - "wordCount" : self.setWordCount, - "paraCount" : self.setParaCount, - "cursorPos" : self.setCursorPos, - } - for xValue in xItem: - if xValue.tag in setMap: - setMap[xValue.tag](xValue.text) - else: - logger.error("Unknown tag '%s'" % xValue.tag) - - return True - - @staticmethod - def _subPack(xParent, name, attrib=None, text=None, none=True): - """Packs the values into an xml element. - """ - if not none and (text == None or text == "None"): - return None - xSub = etree.SubElement(xParent, name, attrib=attrib) - if text is not None: - xSub.text = text - return xSub - - ## - # Set Item Values - ## - - def setName(self, theName): - """Set the item name. - """ - self.itemName = theName.strip() - return - - def setHandle(self, theHandle): - """Set the item handle, and ensure it is valid. - """ - if isinstance(theHandle, str): - if len(theHandle) == 13: - self.itemHandle = theHandle - else: - self.itemHandle = None - else: - self.itemHandle = None - return - - def setParent(self, theParent): - """Set the parent handle, and ensure that it is valid. - """ - if theParent is None: - self.parHandle = None - elif isinstance(theParent, str): - if len(theParent) == 13: - self.parHandle = theParent - else: - self.parHandle = None - else: - self.parHandle = None - return - - def setOrder(self, theOrder): - """Set the item order, and ensure that it is valid. This value - is purely a meta value, not actually used by novelWriter. - """ - self.itemOrder = checkInt(theOrder, 0) - return - - def setType(self, theType): - """Set the item type from either a proper nwItemType, or set it - from a string representing a nwItemType. - """ - if isinstance(theType, nwItemType): - self.itemType = theType - elif theType in nwItemType.__members__: - self.itemType = nwItemType[theType] - else: - logger.error("Unrecognised item type '%s'" % theType) - self.itemType = nwItemType.NO_TYPE - return - - def setClass(self, theClass): - """Set the item class from either a proper nwItemClass, or set - it from a string representing a nwItemClass. - """ - if isinstance(theClass, nwItemClass): - self.itemClass = theClass - elif theClass in nwItemClass.__members__: - self.itemClass = nwItemClass[theClass] - else: - logger.error("Unrecognised item class '%s'" % theClass) - self.itemClass = nwItemClass.NO_CLASS - return - - def setLayout(self, theLayout): - """Set the item layout from either a proper nwItemLayout, or set - it from a string representing a nwItemLayout. - """ - if isinstance(theLayout, nwItemLayout): - self.itemLayout = theLayout - elif theLayout in nwItemLayout.__members__: - self.itemLayout = nwItemLayout[theLayout] - else: - logger.error("Unrecognised item layout '%s'" % theLayout) - self.itemLayout = nwItemLayout.NO_LAYOUT - return - - def setStatus(self, theStatus): - """Set the item status by looking it up in the valid status - items of the current project. - """ - if self.itemClass == nwItemClass.NOVEL: - self.itemStatus = self.theProject.statusItems.checkEntry(theStatus) - else: - self.itemStatus = self.theProject.importItems.checkEntry(theStatus) - return - - def setExpanded(self, expState): - """Save the expanded status of an item in the project tree. - """ - if isinstance(expState, str): - self.isExpanded = expState == str(True) - else: - self.isExpanded = expState == True - return - - def setExported(self, expState): - """Save the export flag. - """ - if isinstance(expState, str): - self.isExported = expState == str(True) - else: - self.isExported = expState == True - return - - ## - # Set Document Meta Data - ## - - def setCharCount(self, theCount): - """Set the character count, and ensure that it is an integer. - """ - self.charCount = checkInt(theCount, 0) - return - - def setWordCount(self, theCount): - """Set the word count, and ensure that it is an integer. - """ - self.wordCount = checkInt(theCount, 0) - return - - def setParaCount(self, theCount): - """Set the paragraph count, and ensure that it is an integer. - """ - self.paraCount = checkInt(theCount, 0) - return - - def setCursorPos(self, thePosition): - """Set the cursor position, and ensure that it is an integer. - """ - self.cursorPos = checkInt(thePosition, 0) - return - -# END Class NWItem - # =============================================================================================== # # NWStatus # Class holding the item status values stored in the NWProject From 017d9c3801eff61b5aafdb2efad466b6a889b789 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:18:49 +0200 Subject: [PATCH 2/5] Moved NWTree to core/tree.py --- nw/core/item.py | 2 +- nw/core/project.py | 387 +---------------------------------------- nw/core/tree.py | 421 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+), 386 deletions(-) create mode 100644 nw/core/tree.py diff --git a/nw/core/item.py b/nw/core/item.py index 5c0f26e0..c9fdfee6 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -3,7 +3,7 @@ novelWriter – Project Item Class ================================== - Class holding the data off a project tree item + Class holding the data of a project tree item File History: Created: 2018-10-27 [0.0.1] NWItem diff --git a/nw/core/project.py b/nw/core/project.py index f6564012..8d9bcec0 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -3,14 +3,13 @@ novelWriter – Project Wrapper =============================== - Class wrapping the data if a novelWriter project + Class wrapping the data of a novelWriter project File History: Created: 2018-09-29 [0.0.1] NWProject Created: 2019-05-19 [0.1.3] NWStatus Created: 2019-10-21 [0.3.1] OptionState Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file - Added: 2020-05-07 [0.4.5] NWTree Rewritten: 2020-02-19 [0.4.5] OptionState This file is a part of novelWriter @@ -42,6 +41,7 @@ from shutil import make_archive from PyQt5.QtWidgets import QMessageBox +from nw.core.tree import NWTree from nw.core.item import NWItem from nw.core.document import NWDoc from nw.common import checkString, checkBool, checkInt, formatTimeStamp @@ -1224,389 +1224,6 @@ class NWProject(): # END Class NWProject -# =============================================================================================== # -# NWTree -# Class holding the project tree for the NWProject -# =============================================================================================== # - -class NWTree(): - - def __init__(self, theProject): - - self.theProject = theProject - - self._projTree = {} # Holds all the items of the project - self._treeOrder = [] # The order of the tree items on the tree view - self._treeRoots = [] # The root items of the tree - self._trashRoot = None # The handle of the trash root folder - self._theLength = 0 # Always the length of _treeOrder - self._theIndex = 0 # The current iterator index - self._treeChanged = False # True if tree structure has changed - self._handleSeed = None # Used for generating handles for testing - - return - - ## - # Class Methods - ## - - def clear(self): - """Clear the item tree entirely. - """ - self._projTree = {} - self._treeOrder = [] - self._treeRoots = [] - self._trashRoot = None - self._theLength = 0 - self._theIndex = 0 - self._treeChanged = False - return - - def handles(self): - """Returns a copy of the list of all the active handles. - """ - return self._treeOrder.copy() - - def append(self, tHandle, pHandle, nwItem): - """Add a new item to the end of the tree. - """ - tHandle = checkString(tHandle, None, True) - pHandle = checkString(pHandle, None, True) - if tHandle is None: - tHandle = self._makeHandle() - - logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle))) - - nwItem.setHandle(tHandle) - nwItem.setParent(pHandle) - - self._projTree[tHandle] = nwItem - self._treeOrder.append(tHandle) - - if nwItem.itemType == nwItemType.ROOT: - logger.verbose("Entry %s is a root item" % str(tHandle)) - self._treeRoots.append(tHandle) - - if nwItem.itemType == nwItemType.TRASH: - if self._trashRoot is None: - logger.verbose("Entry %s is the trash folder" % str(tHandle)) - self._trashRoot = tHandle - else: - logger.error("Only one trash folder allowed") - - self._theLength = len(self._treeOrder) - self._setTreeChanged(True) - - return - - def packXML(self, xParent): - """Pack the content of the tree into an XML object. - """ - xContent = etree.SubElement(xParent, "content", attrib={ - "count":str(self._theLength)} - ) - for tHandle in self._treeOrder: - tItem = self.__getitem__(tHandle) - tItem.packXML(xContent) - return - - def unpackXML(self, xContent): - """Iterate through all items of a content XML object and add - them to the project tree. - """ - if xContent.tag != "content": - logger.error("XML entry is not a NWTree") - return False - - self.clear() - for xItem in xContent: - nwItem = NWItem(self.theProject) - if nwItem.unpackXML(xItem): - self.append(nwItem.itemHandle, nwItem.parHandle, nwItem) - - return True - - def writeToCFiles(self): - """Write the convenience table of contents files in the root of - the project directory. These files are there to assist the user - if they wish to browse the stored files. - """ - tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT) - tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON) - - jsonData = [] - try: - # Dump the text - with open(tocText, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - outFile.write(" Table of Contents\n") - outFile.write("===================\n") - outFile.write("\n") - outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label")) - outFile.write("-"*80+"\n") - for tHandle in sorted(self._treeOrder): - tItem = self.__getitem__(tHandle) - if tItem is None: - continue - tFile = tHandle+".nwd" - if path.isfile(path.join(self.theProject.projContent, tFile)): - outFile.write(" %-25s %-9s %s\n" %( - path.join("content", tFile), - tItem.itemClass.name, - tItem.itemName, - )) - jsonData.append([ - path.join("content", tFile), - tItem.itemClass.name, - tItem.itemName, - ]) - outFile.write("\n") - - # Dump the JSON - with open(tocJson, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps(jsonData, indent=2)) - - except Exception as e: - logger.error(str(e)) - - return - - ## - # Tree Structure Methods - ## - - def trashRoot(self): - """Returns the handle of the trash folder, or None if there - isn't one. - """ - if self._trashRoot: - return self._trashRoot - return None - - def findRoot(self, theClass): - """Find the root item for a given class. - Note: This returns the first item for class CUSTOM. - """ - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return tItem.itemHandle - return None - - def checkRootUnique(self, theClass): - """Checks if there already is a root entry of class 'theClass' - in the root of the project tree. CUSTOM class is skipped as it - is not required to be unique. - """ - if theClass == nwItemClass.CUSTOM: - return True - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if theClass == tItem.itemClass: - return False - return True - - def getRootItem(self, tHandle): - """Iterate upwards in the tree until we find the item with - parent None, the root item. We do this with a for loop with a - maximum depth of 200 to make infinite loops impossible. - """ - tItem = self.__getitem__(tHandle) - if tItem is not None: - for i in range(200): - if tItem.parHandle is None: - return tHandle - else: - tHandle = tItem.parHandle - tItem = self.__getitem__(tHandle) - if tItem is None: - return tHandle - return None - - def getItemPath(self, tHandle): - """Iterate upwards in the tree until we find the item with - parent None, the root item, and return the list of handles. - We do this with a for loop with a maximum depth of 200 to make - infinite loops impossible. - """ - tTree = [] - tItem = self.__getitem__(tHandle) - if tItem is not None: - tTree.append(tHandle) - for i in range(200): - if tItem.parHandle is None: - return tTree - else: - tHandle = tItem.parHandle - tItem = self.__getitem__(tHandle) - if tItem is None: - return tTree - else: - tTree.append(tHandle) - return tTree - - ## - # Setters - ## - - def setOrder(self, newOrder): - """Reorders the tree based on a list of items. - """ - tmpOrder = [] - - # Add all known elements to a new temp list - for tHandle in newOrder: - if tHandle in self._projTree: - tmpOrder.append(tHandle) - else: - logger.error("Handle %s in new tree order is not in project tree" % tHandle) - - # Do a reverse lookup to check for items that will be lost - # This is mainly for debugging purposes - for tHandle in self._treeOrder: - if tHandle not in tmpOrder: - logger.warning("Handle %s in old tree order is not in new tree order" % tHandle) - - # Save the temp list - self._treeOrder = tmpOrder - self._theLength = len(self._treeOrder) - self._setTreeChanged(True) - logger.verbose("Project tree order updated") - - return - - def setSeed(self, theSeed): - """Used for debugging! - Sets a seed for generating handles so that they always come out - in a predictable order. - """ - self._handleSeed = theSeed - return - - ## - # Getters - ## - - def countTypes(self): - """Count the number of files, folders and roots in the project. - """ - nRoot = 0 - nFolder = 0 - nFile = 0 - - for tHandle in self._treeOrder: - tItem = self.__getitem__(tHandle) - if tItem is None: - continue - elif tItem.itemType == nwItemType.ROOT: - nRoot += 1 - elif tItem.itemType == nwItemType.FOLDER: - nFolder += 1 - elif tItem.itemType == nwItemType.FILE: - nFile += 1 - - return nRoot, nFolder, nFile - - ## - # Meta Methods - ## - - def __len__(self): - """Return the length counter. Does not check that it is correct! - """ - return self._theLength - - def __bool__(self): - """Returns True if the tree has any entries. - """ - return self._theLength > 0 - - ## - # Item Access Methods - ## - - def __getitem__(self, tHandle): - """Return a project item based on its handle. Returns None if - the handle doesn't exist in the project. - """ - if tHandle in self._projTree: - return self._projTree[tHandle] - logger.error("No tree item with handle %s" % str(tHandle)) - return None - - def __delitem__(self, tHandle): - """This only removes the item from the order list, but not from - the project tree. - """ - if tHandle not in self._treeOrder: - logger.warning( - "Could not remove item %s from project tree as it does not exist" % tHandle - ) - return False - self._treeOrder.remove(tHandle) - self._theLength = len(self._treeOrder) - self._setTreeChanged(True) - return True - - def __contains__(self, tHandle): - """Checks if a handle exists in the tree. - """ - return tHandle in self._treeOrder - - ## - # Iterator Methods - ## - - def __iter__(self): - """Initiates the iterator. - """ - self._theIndex = 0 - return self - - def __next__(self): - """Returns the item from the next entry in the _treeOrder list. - """ - if self._theIndex < self._theLength: - theItem = self.__getitem__(self._treeOrder[self._theIndex]) - self._theIndex += 1 - return theItem - else: - raise StopIteration - - ## - # Internal Functions - ## - - def _setTreeChanged(self, theState): - """Set the changed flag to theState, and if being set to True, - propagate that state change to the parent NWProject class. - """ - self._treeChanged = theState - if theState: - self.theProject.setProjectChanged(True) - return - - def _makeHandle(self, addSeed=""): - """Generate a unique item handle. In the unlikely event that the - key already exists, salt the seed and generate a new handle. - """ - if self._handleSeed is None: - newSeed = str(time()) + addSeed - else: - # This is used for debugging - newSeed = str(self._handleSeed) - self._handleSeed += 1 - logger.verbose("Generating handle with seed '%s'" % newSeed) - itemHandle = sha256(newSeed.encode()).hexdigest()[0:13] - if itemHandle in self._projTree: - logger.warning("Duplicate handle encountered! Retrying ...") - itemHandle = self._makeHandle(addSeed+"!") - return itemHandle - -# END Class NWTree - # =============================================================================================== # # NWStatus # Class holding the item status values stored in the NWProject diff --git a/nw/core/tree.py b/nw/core/tree.py new file mode 100644 index 00000000..be99f307 --- /dev/null +++ b/nw/core/tree.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Tree Class + + novelWriter – Project Tree Class +================================== + Class holding the data of the project tree + + File History: + Created: 2020-05-07 [0.4.5] NWTree + + This file is a part of novelWriter + Copyright 2020, 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 +import json +import nw + +from os import path +from lxml import etree +from hashlib import sha256 +from time import time + +from PyQt5.QtWidgets import QMessageBox + +from nw.core.item import NWItem +from nw.common import checkString +from nw.constants import nwFiles, nwItemType, nwItemClass + +logger = logging.getLogger(__name__) + +class NWTree(): + + def __init__(self, theProject): + + self.theProject = theProject + + self._projTree = {} # Holds all the items of the project + self._treeOrder = [] # The order of the tree items on the tree view + self._treeRoots = [] # The root items of the tree + self._trashRoot = None # The handle of the trash root folder + self._theLength = 0 # Always the length of _treeOrder + self._theIndex = 0 # The current iterator index + self._treeChanged = False # True if tree structure has changed + self._handleSeed = None # Used for generating handles for testing + + return + + ## + # Class Methods + ## + + def clear(self): + """Clear the item tree entirely. + """ + self._projTree = {} + self._treeOrder = [] + self._treeRoots = [] + self._trashRoot = None + self._theLength = 0 + self._theIndex = 0 + self._treeChanged = False + return + + def handles(self): + """Returns a copy of the list of all the active handles. + """ + return self._treeOrder.copy() + + def append(self, tHandle, pHandle, nwItem): + """Add a new item to the end of the tree. + """ + tHandle = checkString(tHandle, None, True) + pHandle = checkString(pHandle, None, True) + if tHandle is None: + tHandle = self._makeHandle() + + logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle))) + + nwItem.setHandle(tHandle) + nwItem.setParent(pHandle) + + self._projTree[tHandle] = nwItem + self._treeOrder.append(tHandle) + + if nwItem.itemType == nwItemType.ROOT: + logger.verbose("Entry %s is a root item" % str(tHandle)) + self._treeRoots.append(tHandle) + + if nwItem.itemType == nwItemType.TRASH: + if self._trashRoot is None: + logger.verbose("Entry %s is the trash folder" % str(tHandle)) + self._trashRoot = tHandle + else: + logger.error("Only one trash folder allowed") + + self._theLength = len(self._treeOrder) + self._setTreeChanged(True) + + return + + def packXML(self, xParent): + """Pack the content of the tree into an XML object. + """ + xContent = etree.SubElement(xParent, "content", attrib={ + "count":str(self._theLength)} + ) + for tHandle in self._treeOrder: + tItem = self.__getitem__(tHandle) + tItem.packXML(xContent) + return + + def unpackXML(self, xContent): + """Iterate through all items of a content XML object and add + them to the project tree. + """ + if xContent.tag != "content": + logger.error("XML entry is not a NWTree") + return False + + self.clear() + for xItem in xContent: + nwItem = NWItem(self.theProject) + if nwItem.unpackXML(xItem): + self.append(nwItem.itemHandle, nwItem.parHandle, nwItem) + + return True + + def writeToCFiles(self): + """Write the convenience table of contents files in the root of + the project directory. These files are there to assist the user + if they wish to browse the stored files. + """ + tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT) + tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON) + + jsonData = [] + try: + # Dump the text + with open(tocText, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + outFile.write(" Table of Contents\n") + outFile.write("===================\n") + outFile.write("\n") + outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label")) + outFile.write("-"*80+"\n") + for tHandle in sorted(self._treeOrder): + tItem = self.__getitem__(tHandle) + if tItem is None: + continue + tFile = tHandle+".nwd" + if path.isfile(path.join(self.theProject.projContent, tFile)): + outFile.write(" %-25s %-9s %s\n" %( + path.join("content", tFile), + tItem.itemClass.name, + tItem.itemName, + )) + jsonData.append([ + path.join("content", tFile), + tItem.itemClass.name, + tItem.itemName, + ]) + outFile.write("\n") + + # Dump the JSON + with open(tocJson, mode="w+", encoding="utf8") as outFile: + outFile.write(json.dumps(jsonData, indent=2)) + + except Exception as e: + logger.error(str(e)) + + return + + ## + # Tree Structure Methods + ## + + def trashRoot(self): + """Returns the handle of the trash folder, or None if there + isn't one. + """ + if self._trashRoot: + return self._trashRoot + return None + + def findRoot(self, theClass): + """Find the root item for a given class. + Note: This returns the first item for class CUSTOM. + """ + for aRoot in self._treeRoots: + tItem = self.__getitem__(aRoot) + if tItem is None: + continue + if theClass == tItem.itemClass: + return tItem.itemHandle + return None + + def checkRootUnique(self, theClass): + """Checks if there already is a root entry of class 'theClass' + in the root of the project tree. CUSTOM class is skipped as it + is not required to be unique. + """ + if theClass == nwItemClass.CUSTOM: + return True + for aRoot in self._treeRoots: + tItem = self.__getitem__(aRoot) + if theClass == tItem.itemClass: + return False + return True + + def getRootItem(self, tHandle): + """Iterate upwards in the tree until we find the item with + parent None, the root item. We do this with a for loop with a + maximum depth of 200 to make infinite loops impossible. + """ + tItem = self.__getitem__(tHandle) + if tItem is not None: + for i in range(200): + if tItem.parHandle is None: + return tHandle + else: + tHandle = tItem.parHandle + tItem = self.__getitem__(tHandle) + if tItem is None: + return tHandle + return None + + def getItemPath(self, tHandle): + """Iterate upwards in the tree until we find the item with + parent None, the root item, and return the list of handles. + We do this with a for loop with a maximum depth of 200 to make + infinite loops impossible. + """ + tTree = [] + tItem = self.__getitem__(tHandle) + if tItem is not None: + tTree.append(tHandle) + for i in range(200): + if tItem.parHandle is None: + return tTree + else: + tHandle = tItem.parHandle + tItem = self.__getitem__(tHandle) + if tItem is None: + return tTree + else: + tTree.append(tHandle) + return tTree + + ## + # Setters + ## + + def setOrder(self, newOrder): + """Reorders the tree based on a list of items. + """ + tmpOrder = [] + + # Add all known elements to a new temp list + for tHandle in newOrder: + if tHandle in self._projTree: + tmpOrder.append(tHandle) + else: + logger.error("Handle %s in new tree order is not in project tree" % tHandle) + + # Do a reverse lookup to check for items that will be lost + # This is mainly for debugging purposes + for tHandle in self._treeOrder: + if tHandle not in tmpOrder: + logger.warning("Handle %s in old tree order is not in new tree order" % tHandle) + + # Save the temp list + self._treeOrder = tmpOrder + self._theLength = len(self._treeOrder) + self._setTreeChanged(True) + logger.verbose("Project tree order updated") + + return + + def setSeed(self, theSeed): + """Used for debugging! + Sets a seed for generating handles so that they always come out + in a predictable order. + """ + self._handleSeed = theSeed + return + + ## + # Getters + ## + + def countTypes(self): + """Count the number of files, folders and roots in the project. + """ + nRoot = 0 + nFolder = 0 + nFile = 0 + + for tHandle in self._treeOrder: + tItem = self.__getitem__(tHandle) + if tItem is None: + continue + elif tItem.itemType == nwItemType.ROOT: + nRoot += 1 + elif tItem.itemType == nwItemType.FOLDER: + nFolder += 1 + elif tItem.itemType == nwItemType.FILE: + nFile += 1 + + return nRoot, nFolder, nFile + + ## + # Meta Methods + ## + + def __len__(self): + """Return the length counter. Does not check that it is correct! + """ + return self._theLength + + def __bool__(self): + """Returns True if the tree has any entries. + """ + return self._theLength > 0 + + ## + # Item Access Methods + ## + + def __getitem__(self, tHandle): + """Return a project item based on its handle. Returns None if + the handle doesn't exist in the project. + """ + if tHandle in self._projTree: + return self._projTree[tHandle] + logger.error("No tree item with handle %s" % str(tHandle)) + return None + + def __delitem__(self, tHandle): + """This only removes the item from the order list, but not from + the project tree. + """ + if tHandle not in self._treeOrder: + logger.warning( + "Could not remove item %s from project tree as it does not exist" % tHandle + ) + return False + self._treeOrder.remove(tHandle) + self._theLength = len(self._treeOrder) + self._setTreeChanged(True) + return True + + def __contains__(self, tHandle): + """Checks if a handle exists in the tree. + """ + return tHandle in self._treeOrder + + ## + # Iterator Methods + ## + + def __iter__(self): + """Initiates the iterator. + """ + self._theIndex = 0 + return self + + def __next__(self): + """Returns the item from the next entry in the _treeOrder list. + """ + if self._theIndex < self._theLength: + theItem = self.__getitem__(self._treeOrder[self._theIndex]) + self._theIndex += 1 + return theItem + else: + raise StopIteration + + ## + # Internal Functions + ## + + def _setTreeChanged(self, theState): + """Set the changed flag to theState, and if being set to True, + propagate that state change to the parent NWProject class. + """ + self._treeChanged = theState + if theState: + self.theProject.setProjectChanged(True) + return + + def _makeHandle(self, addSeed=""): + """Generate a unique item handle. In the unlikely event that the + key already exists, salt the seed and generate a new handle. + """ + if self._handleSeed is None: + newSeed = str(time()) + addSeed + else: + # This is used for debugging + newSeed = str(self._handleSeed) + self._handleSeed += 1 + logger.verbose("Generating handle with seed '%s'" % newSeed) + itemHandle = sha256(newSeed.encode()).hexdigest()[0:13] + if itemHandle in self._projTree: + logger.warning("Duplicate handle encountered! Retrying ...") + itemHandle = self._makeHandle(addSeed+"!") + return itemHandle + +# END Class NWTree From 59795e62f746a49147a7620b7f047d6ef965f1e9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:23:03 +0200 Subject: [PATCH 3/5] Moved NWStatus to core/status.py --- nw/core/item.py | 2 +- nw/core/project.py | 167 +------------------------------------- nw/core/status.py | 194 +++++++++++++++++++++++++++++++++++++++++++++ nw/core/tree.py | 4 +- 4 files changed, 197 insertions(+), 170 deletions(-) create mode 100644 nw/core/status.py diff --git a/nw/core/item.py b/nw/core/item.py index c9fdfee6..4dd9ddb9 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -6,7 +6,7 @@ Class holding the data of a project tree item File History: - Created: 2018-10-27 [0.0.1] NWItem + Created: 2018-10-27 [0.0.1] This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen diff --git a/nw/core/project.py b/nw/core/project.py index 8d9bcec0..534a2554 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -7,9 +7,7 @@ File History: Created: 2018-09-29 [0.0.1] NWProject - Created: 2019-05-19 [0.1.3] NWStatus Created: 2019-10-21 [0.3.1] OptionState - Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file Rewritten: 2020-02-19 [0.4.5] OptionState This file is a part of novelWriter @@ -44,6 +42,7 @@ from PyQt5.QtWidgets import QMessageBox from nw.core.tree import NWTree from nw.core.item import NWItem from nw.core.document import NWDoc +from nw.core.status import NWStatus from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -1224,170 +1223,6 @@ class NWProject(): # END Class NWProject -# =============================================================================================== # -# NWStatus -# Class holding the item status values stored in the NWProject -# =============================================================================================== # - -class NWStatus(): - - def __init__(self): - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 - return - - def addEntry(self, theLabel, theColours): - """Add a status entry to the status object, but ensure it isn't - a duplicate. - """ - theLabel = theLabel.strip() - if self.lookupEntry(theLabel) is None: - self.theLabels.append(theLabel) - self.theColours.append(theColours) - self.theCounts.append(0) - self.theMap[theLabel] = self.theLength - self.theLength += 1 - return True - - def lookupEntry(self, theLabel): - """Look up a status entry in the object lists, and return it if - it exists. - """ - if theLabel is None: - return None - theLabel = theLabel.strip() - if theLabel in self.theMap.keys(): - return self.theMap[theLabel] - return None - - def checkEntry(self, theStatus): - """Check if a status value is valid, and returns the safe - reference to be used internally. - """ - if isinstance(theStatus, str): - theStatus = theStatus.strip() - if self.lookupEntry(theStatus) is not None: - return theStatus - theStatus = checkInt(theStatus, 0, False) - if theStatus >= 0 and theStatus < self.theLength: - return self.theLabels[theStatus] - - def setNewEntries(self, newList): - """Update the list of entries after they have been modified by - the GUI tool. - """ - replaceMap = {} - - if newList is not None: - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 - - for nName, nR, nG, nB, oName in newList: - self.addEntry(nName, (nR, nG, nB)) - if nName != oName and oName is not None: - replaceMap[oName] = nName - - return replaceMap - - def resetCounts(self): - """Clear the counts of references to the status entries. - """ - self.theCounts = [0]*self.theLength - return - - def countEntry(self, theLabel): - """Lookup the usage count of a given entry. - """ - theIndex = self.lookupEntry(theLabel) - if theIndex is not None: - self.theCounts[theIndex] += 1 - return - - def packEntries(self, xParent): - """Pack the status entries into an XML object for saving to the - main project file. - """ - for n in range(self.theLength): - xSub = etree.SubElement(xParent,"entry",attrib={ - "blue" : str(self.theColours[n][2]), - "green" : str(self.theColours[n][1]), - "red" : str(self.theColours[n][0]), - }) - xSub.text = self.theLabels[n] - return True - - def unpackEntries(self, xParent): - """Unpack an XML tree and set the class values. - """ - theLabels = [] - theColours = [] - - for xChild in xParent: - theLabels.append(xChild.text) - if "red" in xChild.attrib: - cR = checkInt(xChild.attrib["red"],0,False) - else: - cR = 0 - if "green" in xChild.attrib: - cG = checkInt(xChild.attrib["green"],0,False) - else: - cG = 0 - if "blue" in xChild.attrib: - cB = checkInt(xChild.attrib["blue"],0,False) - else: - cB = 0 - theColours.append((cR,cG,cB)) - - if len(theLabels) > 0: - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 - - for n in range(len(theLabels)): - self.addEntry(theLabels[n], theColours[n]) - - return True - - ## - # Iterator Bits - ## - - def __getitem__(self, n): - """Return an entry by its index. - """ - if n >= 0 and n < self.theLength: - return self.theLabels[n], self.theColours[n], self.theCounts[n] - return None, None, None - - def __iter__(self): - """Initialise the iterator. - """ - self.theIndex = 0 - return self - - def __next__(self): - """Return the next entry for the iterator. - """ - if self.theIndex < self.theLength: - theLabel, theColour, theCount = self.__getitem__(self.theIndex) - self.theIndex += 1 - return theLabel, theColour, theCount - else: - raise StopIteration - -# END Class NWStatus - # =============================================================================================== # # OptionState # Save the project-wise state of options that don't go into project XML or main config diff --git a/nw/core/status.py b/nw/core/status.py new file mode 100644 index 00000000..3dd26b35 --- /dev/null +++ b/nw/core/status.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Item Status Class + + novelWriter – Project Item Status Class +========================================= + Class holding the status elements of a project item + + File History: + Created: 2019-05-19 [0.1.3] + + This file is a part of novelWriter + Copyright 2020, 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 +import nw + +from lxml import etree + +from nw.common import checkInt + +logger = logging.getLogger(__name__) + +class NWStatus(): + + def __init__(self): + self.theLabels = [] + self.theColours = [] + self.theCounts = [] + self.theMap = {} + self.theLength = 0 + self.theIndex = 0 + return + + def addEntry(self, theLabel, theColours): + """Add a status entry to the status object, but ensure it isn't + a duplicate. + """ + theLabel = theLabel.strip() + if self.lookupEntry(theLabel) is None: + self.theLabels.append(theLabel) + self.theColours.append(theColours) + self.theCounts.append(0) + self.theMap[theLabel] = self.theLength + self.theLength += 1 + return True + + def lookupEntry(self, theLabel): + """Look up a status entry in the object lists, and return it if + it exists. + """ + if theLabel is None: + return None + theLabel = theLabel.strip() + if theLabel in self.theMap.keys(): + return self.theMap[theLabel] + return None + + def checkEntry(self, theStatus): + """Check if a status value is valid, and returns the safe + reference to be used internally. + """ + if isinstance(theStatus, str): + theStatus = theStatus.strip() + if self.lookupEntry(theStatus) is not None: + return theStatus + theStatus = checkInt(theStatus, 0, False) + if theStatus >= 0 and theStatus < self.theLength: + return self.theLabels[theStatus] + + def setNewEntries(self, newList): + """Update the list of entries after they have been modified by + the GUI tool. + """ + replaceMap = {} + + if newList is not None: + self.theLabels = [] + self.theColours = [] + self.theCounts = [] + self.theMap = {} + self.theLength = 0 + self.theIndex = 0 + + for nName, nR, nG, nB, oName in newList: + self.addEntry(nName, (nR, nG, nB)) + if nName != oName and oName is not None: + replaceMap[oName] = nName + + return replaceMap + + def resetCounts(self): + """Clear the counts of references to the status entries. + """ + self.theCounts = [0]*self.theLength + return + + def countEntry(self, theLabel): + """Lookup the usage count of a given entry. + """ + theIndex = self.lookupEntry(theLabel) + if theIndex is not None: + self.theCounts[theIndex] += 1 + return + + def packEntries(self, xParent): + """Pack the status entries into an XML object for saving to the + main project file. + """ + for n in range(self.theLength): + xSub = etree.SubElement(xParent,"entry",attrib={ + "blue" : str(self.theColours[n][2]), + "green" : str(self.theColours[n][1]), + "red" : str(self.theColours[n][0]), + }) + xSub.text = self.theLabels[n] + return True + + def unpackEntries(self, xParent): + """Unpack an XML tree and set the class values. + """ + theLabels = [] + theColours = [] + + for xChild in xParent: + theLabels.append(xChild.text) + if "red" in xChild.attrib: + cR = checkInt(xChild.attrib["red"],0,False) + else: + cR = 0 + if "green" in xChild.attrib: + cG = checkInt(xChild.attrib["green"],0,False) + else: + cG = 0 + if "blue" in xChild.attrib: + cB = checkInt(xChild.attrib["blue"],0,False) + else: + cB = 0 + theColours.append((cR,cG,cB)) + + if len(theLabels) > 0: + self.theLabels = [] + self.theColours = [] + self.theCounts = [] + self.theMap = {} + self.theLength = 0 + self.theIndex = 0 + + for n in range(len(theLabels)): + self.addEntry(theLabels[n], theColours[n]) + + return True + + ## + # Iterator Bits + ## + + def __getitem__(self, n): + """Return an entry by its index. + """ + if n >= 0 and n < self.theLength: + return self.theLabels[n], self.theColours[n], self.theCounts[n] + return None, None, None + + def __iter__(self): + """Initialise the iterator. + """ + self.theIndex = 0 + return self + + def __next__(self): + """Return the next entry for the iterator. + """ + if self.theIndex < self.theLength: + theLabel, theColour, theCount = self.__getitem__(self.theIndex) + self.theIndex += 1 + return theLabel, theColour, theCount + else: + raise StopIteration + +# END Class NWStatus diff --git a/nw/core/tree.py b/nw/core/tree.py index be99f307..7f1482fe 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -6,7 +6,7 @@ Class holding the data of the project tree File History: - Created: 2020-05-07 [0.4.5] NWTree + Created: 2020-05-07 [0.4.5] This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -34,8 +34,6 @@ from lxml import etree from hashlib import sha256 from time import time -from PyQt5.QtWidgets import QMessageBox - from nw.core.item import NWItem from nw.common import checkString from nw.constants import nwFiles, nwItemType, nwItemClass From da0d4017cc7276aff9107ba60323791c500a3111 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:27:23 +0200 Subject: [PATCH 4/5] Moved OptionState to core/options.py --- nw/core/options.py | 251 +++++++++++++++++++++++++++++++++++++++++++++ nw/core/project.py | 220 +-------------------------------------- 2 files changed, 252 insertions(+), 219 deletions(-) create mode 100644 nw/core/options.py diff --git a/nw/core/options.py b/nw/core/options.py new file mode 100644 index 00000000..12593c0c --- /dev/null +++ b/nw/core/options.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Options Cache + + novelWriter – Project Options Cache +===================================== + Class wrapping the project options state + + File History: + Created: 2019-10-21 [0.3.1] + Rewritten: 2020-02-19 [0.4.5] + + This file is a part of novelWriter + Copyright 2020, 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 +import json +import nw + +from os import path + +from nw.constants import nwFiles + +logger = logging.getLogger(__name__) + +class OptionState(): + + def __init__(self, theProject): + + self.mainConf = nw.CONFIG + self.theProject = theProject + + self.theState = {} + self.validMap = { + "GuiSession": set([ + "widthCol0", + "widthCol1", + "widthCol2", + "sortCol", + "sortOrder", + "hideZeros", + "hideNegative", + ]), + "GuiDocSplit": set([ + "spLevel", + ]), + "GuiBuildNovel": set([ + "winWidth", + "winHeight", + "addNovel", + "addNotes", + "ignoreFlag", + "justifyText", + "excludeBody", + "textFont", + "textSize", + "noStyling", + "incSynopsis", + "incComments", + "incKeywords", + "incBodyText", + ]), + "GuiOutline": set([ + "headerOrder", + "columnWidth", + "columnHidden", + ]) + } + + return + + ## + # Load and Save Cache + ## + + def loadSettings(self): + """Load the options dictionary from the project settings file. + """ + if self.theProject.projMeta is None: + return False + + stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) + theState = {} + + if path.isfile(stateFile): + logger.debug("Loading GUI options file") + try: + with open(stateFile, mode="r", encoding="utf8") as inFile: + theJson = inFile.read() + theState = json.loads(theJson) + except Exception as e: + logger.error("Failed to load GUI options file") + logger.error(str(e)) + return False + + # Filter out unused variables + for aGroup in theState: + if aGroup in self.validMap: + self.theState[aGroup] = {} + for anOpt in theState[aGroup]: + if anOpt in self.validMap[aGroup]: + self.theState[aGroup][anOpt] = theState[aGroup][anOpt] + + return True + + def saveSettings(self): + """Save the options dictionary to the project settings file. + """ + if self.theProject.projMeta is None: + return False + + stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) + logger.debug("Saving GUI options file") + + try: + with open(stateFile, mode="w+", encoding="utf8") as outFile: + outFile.write(json.dumps(self.theState, indent=2)) + except Exception as e: + logger.error("Failed to save GUI options file") + logger.error(str(e)) + return False + + return True + + ## + # Setters + ## + + def setValue(self, setGroup, setName, setValue): + """Saves a value, with a given group and name. + """ + if not setGroup in self.validMap: + logger.error("Unknown option group '%s'" % setGroup) + return False + + if not setName in self.validMap[setGroup]: + logger.error("Unknown option name '%s'" % setName) + return False + + if not setGroup in self.theState: + self.theState[setGroup] = {} + + self.theState[setGroup][setName] = setValue + + return True + + ## + # Getters + ## + + def getValue(self, getGroup, getName, defaultValue): + """Return an arbitrary type value, if it exists. Otherwise, + return the default value. + """ + if getGroup in self.theState: + if getName in self.theState[getGroup]: + try: + return self.theState[getGroup][getName] + except Exception as e: + logger.warning(str(e)) + return defaultValue + return defaultValue + + def getString(self, getGroup, getName, defaultValue): + """Return the value as a string, if it exists. Otherwise, return + the default value. + """ + if getGroup in self.theState: + if getName in self.theState[getGroup]: + try: + return str(self.theState[getGroup][getName]) + except Exception as e: + logger.warning(str(e)) + return defaultValue + return defaultValue + + def getInt(self, getGroup, getName, defaultValue): + """Return the value as an int, if it exists. Otherwise, return + the default value. + """ + if getGroup in self.theState: + if getName in self.theState[getGroup]: + try: + return int(self.theState[getGroup][getName]) + except Exception as e: + logger.warning(str(e)) + return defaultValue + return defaultValue + + def getFloat(self, getGroup, getName, defaultValue): + """Return the value as a float, if it exists. Otherwise, return + the default value. + """ + if getGroup in self.theState: + if getName in self.theState[getGroup]: + try: + return float(self.theState[getGroup][getName]) + except Exception as e: + logger.warning(str(e)) + return defaultValue + return defaultValue + + def getBool(self, getGroup, getName, defaultValue): + """Return the value as a bool, if it exists. Otherwise, return + the default value. + """ + if getGroup in self.theState: + if getName in self.theState[getGroup]: + try: + return bool(self.theState[getGroup][getName]) + except Exception as e: + logger.warning(str(e)) + return defaultValue + return defaultValue + + ## + # Validators + ## + + def validIntRange(self, theValue, intA, intB, intDefault): + """Check that an int is in a given range. If it isn't, return + the default value. + """ + if isinstance(theValue, int): + if theValue >= intA and theValue <= intB: + return theValue + return intDefault + + def validIntTuple(self, theValue, theTuple, intDefault): + """Check that an int is an element of a tuple. If it isn't, + return the default value. + """ + if isinstance(theValue, int): + if theValue in theTuple: + return theValue + return intDefault + +# END Class OptionState diff --git a/nw/core/project.py b/nw/core/project.py index 534a2554..2f700f4f 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -43,6 +43,7 @@ from nw.core.tree import NWTree from nw.core.item import NWItem from nw.core.document import NWDoc from nw.core.status import NWStatus +from nw.core.options import OptionState from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -1222,222 +1223,3 @@ class NWProject(): return # END Class NWProject - -# =============================================================================================== # -# OptionState -# Save the project-wise state of options that don't go into project XML or main config -# =============================================================================================== # - -class OptionState(): - - def __init__(self, theProject): - - self.mainConf = nw.CONFIG - self.theProject = theProject - - self.theState = {} - self.validMap = { - "GuiSession": set([ - "widthCol0", - "widthCol1", - "widthCol2", - "sortCol", - "sortOrder", - "hideZeros", - "hideNegative", - ]), - "GuiDocSplit": set([ - "spLevel", - ]), - "GuiBuildNovel": set([ - "winWidth", - "winHeight", - "addNovel", - "addNotes", - "ignoreFlag", - "justifyText", - "excludeBody", - "textFont", - "textSize", - "noStyling", - "incSynopsis", - "incComments", - "incKeywords", - "incBodyText", - ]), - "GuiOutline": set([ - "headerOrder", - "columnWidth", - "columnHidden", - ]) - } - - return - - ## - # Load and Save Cache - ## - - def loadSettings(self): - """Load the options dictionary from the project settings file. - """ - if self.theProject.projMeta is None: - return False - - stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) - theState = {} - - if path.isfile(stateFile): - logger.debug("Loading GUI options file") - try: - with open(stateFile, mode="r", encoding="utf8") as inFile: - theJson = inFile.read() - theState = json.loads(theJson) - except Exception as e: - logger.error("Failed to load GUI options file") - logger.error(str(e)) - return False - - # Filter out unused variables - for aGroup in theState: - if aGroup in self.validMap: - self.theState[aGroup] = {} - for anOpt in theState[aGroup]: - if anOpt in self.validMap[aGroup]: - self.theState[aGroup][anOpt] = theState[aGroup][anOpt] - - return True - - def saveSettings(self): - """Save the options dictionary to the project settings file. - """ - if self.theProject.projMeta is None: - return False - - stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) - logger.debug("Saving GUI options file") - - try: - with open(stateFile, mode="w+", encoding="utf8") as outFile: - outFile.write(json.dumps(self.theState, indent=2)) - except Exception as e: - logger.error("Failed to save GUI options file") - logger.error(str(e)) - return False - - return True - - ## - # Setters - ## - - def setValue(self, setGroup, setName, setValue): - """Saves a value, with a given group and name. - """ - if not setGroup in self.validMap: - logger.error("Unknown option group '%s'" % setGroup) - return False - - if not setName in self.validMap[setGroup]: - logger.error("Unknown option name '%s'" % setName) - return False - - if not setGroup in self.theState: - self.theState[setGroup] = {} - - self.theState[setGroup][setName] = setValue - - return True - - ## - # Getters - ## - - def getValue(self, getGroup, getName, defaultValue): - """Return an arbitrary type value, if it exists. Otherwise, - return the default value. - """ - if getGroup in self.theState: - if getName in self.theState[getGroup]: - try: - return self.theState[getGroup][getName] - except Exception as e: - logger.warning(str(e)) - return defaultValue - return defaultValue - - def getString(self, getGroup, getName, defaultValue): - """Return the value as a string, if it exists. Otherwise, return - the default value. - """ - if getGroup in self.theState: - if getName in self.theState[getGroup]: - try: - return str(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue - return defaultValue - - def getInt(self, getGroup, getName, defaultValue): - """Return the value as an int, if it exists. Otherwise, return - the default value. - """ - if getGroup in self.theState: - if getName in self.theState[getGroup]: - try: - return int(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue - return defaultValue - - def getFloat(self, getGroup, getName, defaultValue): - """Return the value as a float, if it exists. Otherwise, return - the default value. - """ - if getGroup in self.theState: - if getName in self.theState[getGroup]: - try: - return float(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue - return defaultValue - - def getBool(self, getGroup, getName, defaultValue): - """Return the value as a bool, if it exists. Otherwise, return - the default value. - """ - if getGroup in self.theState: - if getName in self.theState[getGroup]: - try: - return bool(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue - return defaultValue - - ## - # Validators - ## - - def validIntRange(self, theValue, intA, intB, intDefault): - """Check that an int is in a given range. If it isn't, return - the default value. - """ - if isinstance(theValue, int): - if theValue >= intA and theValue <= intB: - return theValue - return intDefault - - def validIntTuple(self, theValue, theTuple, intDefault): - """Check that an int is an element of a tuple. If it isn't, - return the default value. - """ - if isinstance(theValue, int): - if theValue in theTuple: - return theValue - return intDefault - -# END Class OptionState From 260b2aa7b9711e88b6b3f49f07592e8522665900 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:31:17 +0200 Subject: [PATCH 5/5] Some final cleanup --- nw/core/project.py | 5 +---- nw/gui/__init__.py | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 2f700f4f..45480820 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -6,9 +6,7 @@ Class wrapping the data of a novelWriter project File History: - Created: 2018-09-29 [0.0.1] NWProject - Created: 2019-10-21 [0.3.1] OptionState - Rewritten: 2020-02-19 [0.4.5] OptionState + Created: 2018-09-29 [0.0.1] This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -33,7 +31,6 @@ import nw from os import path, mkdir, listdir, unlink, rename, rmdir from lxml import etree -from hashlib import sha256 from time import time from shutil import make_archive diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 981173f2..3f3d056a 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -2,9 +2,7 @@ from nw.gui.about import GuiAbout from nw.gui.build import GuiBuildNovel -from nw.gui.docbars import GuiDocTitleBar -from nw.gui.docbars import GuiNoticeBar -from nw.gui.docbars import GuiSearchBar +from nw.gui.docbars import GuiDocTitleBar, GuiNoticeBar, GuiSearchBar from nw.gui.docdetails import GuiDocViewDetails from nw.gui.doceditor import GuiDocEditor from nw.gui.docmerge import GuiDocMerge @@ -21,8 +19,7 @@ from nw.gui.projsettings import GuiProjectSettings from nw.gui.projtree import GuiProjectTree from nw.gui.sessionlog import GuiSessionLogView from nw.gui.statusbar import GuiMainStatus -from nw.gui.theme import GuiIcons -from nw.gui.theme import GuiTheme +from nw.gui.theme import GuiIcons, GuiTheme __all__ = [ "GuiAbout",