Merge pull request #289 from vkbo/project_split

Project File Split
This commit is contained in:
Veronica K. Berglyd Olsen
2020-06-06 15:36:44 +02:00
committed by GitHub
6 changed files with 1149 additions and 1027 deletions
+277
View File
@@ -0,0 +1,277 @@
# -*- coding: utf-8 -*-
"""novelWriter Project Item Class
novelWriter Project Item Class
==================================
Class holding the data of a project tree item
File History:
Created: 2018-10-27 [0.0.1]
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 <https://www.gnu.org/licenses/>.
"""
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
+251
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+6 -1022
View File
File diff suppressed because it is too large Load Diff
+194
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+419
View File
@@ -0,0 +1,419 @@
# -*- 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]
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 <https://www.gnu.org/licenses/>.
"""
import logging
import json
import nw
from os import path
from lxml import etree
from hashlib import sha256
from time import time
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
+2 -5
View File
@@ -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",