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",