Files
novelWriter/nw/core/tree.py
T
2020-06-06 15:23:03 +02:00

420 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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