diff --git a/nw/convert/file/text.py b/nw/convert/file/text.py index f5119781..ceab8d55 100644 --- a/nw/convert/file/text.py +++ b/nw/convert/file/text.py @@ -164,12 +164,12 @@ class TextFile(): * Items that appear in the TRASH folder """ - theItem = self.theProject.getItem(tHandle) + theItem = self.theProject.projTree[tHandle] isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.itemClass == nwItemClass.TRASH - isNone |= theItem.parHandle == self.theProject.trashRoot + isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() isNote = theItem.itemLayout == nwItemLayout.NOTE isNovel = not isNone and not isNote diff --git a/nw/convert/tokenizer.py b/nw/convert/tokenizer.py index 53f44958..f891a9f7 100644 --- a/nw/convert/tokenizer.py +++ b/nw/convert/tokenizer.py @@ -142,7 +142,7 @@ class Tokenizer(): def setText(self, theHandle, theText=None): self.theHandle = theHandle - self.theItem = self.theProject.getItem(theHandle) + self.theItem = self.theProject.projTree[theHandle] if theText is not None: # If the text is set, just use that diff --git a/nw/gui/dialogs/docmerge.py b/nw/gui/dialogs/docmerge.py index 02bcbf5b..887e8b72 100644 --- a/nw/gui/dialogs/docmerge.py +++ b/nw/gui/dialogs/docmerge.py @@ -115,7 +115,7 @@ class GuiDocMerge(QDialog): ), nwAlert.ERROR) return - srcItem = self.theProject.getItem(self.sourceItem) + srcItem = self.theProject.projTree[self.sourceItem] nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) self.theParent.treeView.revealTreeItem(nHandle) theDoc.openDocument(nHandle, False) @@ -149,7 +149,7 @@ class GuiDocMerge(QDialog): if tHandle is None: return - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem is None: return if nwItem.itemType is not nwItemType.FOLDER: @@ -160,7 +160,7 @@ class GuiDocMerge(QDialog): for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() - nwItem = self.theProject.getItem(sHandle) + nwItem = self.theProject.projTree[sHandle] if nwItem.itemType is not nwItemType.FILE: continue newItem.setText(nwItem.itemName) diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index 3a2b332e..55be4ee4 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -120,7 +120,7 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - srcItem = self.theProject.getItem(self.sourceItem) + srcItem = self.theProject.projTree[self.sourceItem] if srcItem is None: self.theParent.makeAlert(( "Could not parse source document." @@ -172,7 +172,7 @@ class GuiDocSplit(QDialog): wTitle = wTitle.strip() nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) - newItem = self.theProject.getItem(nHandle) + newItem = self.theProject.projTree[nHandle] newItem.setLayout(itemLayout) logger.verbose( "Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1) @@ -213,7 +213,7 @@ class GuiDocSplit(QDialog): if self.sourceItem is None: return - nwItem = self.theProject.getItem(self.sourceItem) + nwItem = self.theProject.projTree[self.sourceItem] if nwItem is None: return if nwItem.itemType is not nwItemType.FILE: diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py index eb8f3925..2d67e482 100644 --- a/nw/gui/dialogs/export.py +++ b/nw/gui/dialogs/export.py @@ -134,7 +134,7 @@ class GuiExport(QDialog): self.exportStatus.setText("Export failed ...") return False - nItems = len(self.theProject.treeOrder) + nItems = len(self.theProject.projTree) if eFormat == GuiExportMain.FMT_PDOC: nItems += int(0.2*nItems) self.exportProgress.setMinimum(0) @@ -182,16 +182,14 @@ class GuiExport(QDialog): time.sleep(0.5) nDone = 0 - for tHandle in self.theProject.treeOrder: + for tItem in self.theProject.projTree: self.exportProgress.setValue(nDone) - tItem = self.theProject.getItem(tHandle) - self.exportStatus.setText("Exporting: %s" % tItem.itemName) logger.verbose("Exporting: %s" % tItem.itemName) if tItem is not None and tItem.itemType == nwItemType.FILE: - outFile.addText(tHandle) + outFile.addText(tItem.itemHandle) nDone += 1 diff --git a/nw/gui/dialogs/itemeditor.py b/nw/gui/dialogs/itemeditor.py index f58ee020..30c4721c 100644 --- a/nw/gui/dialogs/itemeditor.py +++ b/nw/gui/dialogs/itemeditor.py @@ -48,7 +48,7 @@ class GuiItemEditor(QDialog): self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent - self.theItem = self.theProject.getItem(tHandle) + self.theItem = self.theProject.projTree[tHandle] self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() diff --git a/nw/gui/elements/docdetails.py b/nw/gui/elements/docdetails.py index b638ea79..a55e44c2 100644 --- a/nw/gui/elements/docdetails.py +++ b/nw/gui/elements/docdetails.py @@ -83,7 +83,7 @@ class GuiDocDetails(QFrame): def buildViewBox(self, tHandle): - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem is None: colTwo = [""]*4 diff --git a/nw/gui/elements/doctitlebar.py b/nw/gui/elements/doctitlebar.py index 8547ad06..9e05f479 100644 --- a/nw/gui/elements/doctitlebar.py +++ b/nw/gui/elements/doctitlebar.py @@ -87,15 +87,15 @@ class GuiDocTitleBar(QLabel): if self.mainConf.showFullPath: tTitle = [] - tTree = self.theProject.getItemPath(tHandle) + tTree = self.theProject.projTree.getItemPath(tHandle) for aHandle in reversed(tTree): - nwItem = self.theProject.getItem(aHandle) + nwItem = self.theProject.projTree[aHandle] if nwItem is not None: tTitle.append(nwItem.itemName) sSep = " %s " % nwUnicode.U_RSAQUO self.setText(sSep.join(tTitle)) else: - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem is None: return False diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index b6858905..09330346 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication, QMessageBox ) -from nw.project import NWItem, NWDoc +from nw.project import NWDoc from nw.constants import ( nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert ) @@ -118,7 +118,7 @@ class GuiDocTree(QTreeWidget): return False if itemClass is None and pHandle is not None: - pItem = self.theProject.getItem(pHandle) + pItem = self.theProject.projTree[pHandle] if pItem is not None: itemClass = pItem.itemClass @@ -150,7 +150,7 @@ class GuiDocTree(QTreeWidget): # If no parent has been selected, make the new file under # the root NOVEL item. if pHandle is None: - pHandle = self.theProject.findRootItem(nwItemClass.NOVEL) + pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL) # If still nothing, give up if pHandle is None: @@ -159,7 +159,7 @@ class GuiDocTree(QTreeWidget): # Now check if the selected item is a file, in which case # the new file will be a sibling - pItem = self.theProject.getItem(pHandle) + pItem = self.theProject.projTree[pHandle] if pItem.itemType == nwItemType.FILE: pHandle = pItem.parHandle @@ -170,7 +170,7 @@ class GuiDocTree(QTreeWidget): ) return False - if pHandle == self.theProject.trashRoot: + if pHandle == self.theProject.projTree.trashRoot(): self.makeAlert( "Cannot add new files or folders to the trash folder.", nwAlert.ERROR ) @@ -194,7 +194,7 @@ class GuiDocTree(QTreeWidget): def revealTreeItem(self, tHandle): """Reveal a newly added project item in the project tree. """ - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] trItem = self._addTreeItem(nwItem) pHandle = nwItem.parHandle if pHandle is not None and pHandle in self.theMap.keys(): @@ -267,14 +267,16 @@ class GuiDocTree(QTreeWidget): deleteItem function for each document in the Trash folder. """ + trashHandle = self.theProject.projTree.trashRoot() + logger.debug("Emptying Trash folder") - if self.theProject.trashRoot is None: + if trashHandle is None: self.makeAlert("There is no Trash folder.", nwAlert.INFO) return False - theTrash = self.getTreeFromHandle(self.theProject.trashRoot) - if self.theProject.trashRoot in theTrash: - theTrash.remove(self.theProject.trashRoot) + theTrash = self.getTreeFromHandle(trashHandle) + if trashHandle in theTrash: + theTrash.remove(trashHandle) nTrash = len(theTrash) if nTrash == 0: @@ -291,8 +293,8 @@ class GuiDocTree(QTreeWidget): return False logger.verbose("Deleting %d files from Trash" % nTrash) - for tHandle in self.getTreeFromHandle(self.theProject.trashRoot): - if tHandle == self.theProject.trashRoot: + for tHandle in self.getTreeFromHandle(trashHandle): + if tHandle == trashHandle: continue self.deleteItem(tHandle, True) @@ -313,7 +315,7 @@ class GuiDocTree(QTreeWidget): return False trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.getItem(tHandle) + nwItemS = self.theProject.projTree[tHandle] if nwItemS is None: return False @@ -327,7 +329,7 @@ class GuiDocTree(QTreeWidget): return False pHandle = nwItemS.parHandle - if pHandle is not None and pHandle == self.theProject.trashRoot: + if pHandle is not None and pHandle == self.theProject.projTree.trashRoot(): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. @@ -353,7 +355,7 @@ class GuiDocTree(QTreeWidget): theDoc = NWDoc(self.theProject, self.theParent) theDoc.deleteDocument(tHandle) - self.theProject.deleteItem(tHandle) + del self.theProject.projTree[tHandle] self.theParent.theIndex.deleteHandle(tHandle) else: @@ -366,7 +368,7 @@ class GuiDocTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - nwItemS.setParent(self.theProject.trashRoot) + nwItemS.setParent(self.theProject.projTree.trashRoot()) self.theProject.setProjectChanged(True) self.theParent.theIndex.deleteHandle(tHandle) @@ -380,7 +382,7 @@ class GuiDocTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) if trItemS.childCount() == 0: trItemP.takeChild(tIndex) - self.theProject.deleteItem(tHandle) + del self.theProject.projTree[tHandle] else: self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR) return False @@ -401,7 +403,7 @@ class GuiDocTree(QTreeWidget): def setTreeItemValues(self, tHandle): trItem = self._getTreeItem(tHandle) - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] tName = nwItem.itemName tClass = nwItem.itemClass tHandle = nwItem.itemHandle @@ -552,14 +554,15 @@ class GuiDocTree(QTreeWidget): """Adds the trash root folder if it doesn't already exist in the project tree. """ - if self.theProject.trashRoot is None: + trashHandle = self.theProject.projTree.trashRoot() + if trashHandle is None: self.theProject.addTrash() trItem = self._addTreeItem( - self.theProject.getItem(self.theProject.trashRoot) + self.theProject.projTree[trashHandle] ) trItem.setExpanded(True) else: - trItem = self._getTreeItem(self.theProject.trashRoot) + trItem = self._getTreeItem(trashHandle) return trItem def _addOrphanedRoot(self): @@ -589,7 +592,7 @@ class GuiDocTree(QTreeWidget): """ trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.getItem(tHandle) + nwItemS = self.theProject.projTree[tHandle] trItemP = trItemS.parent() if trItemP is None: logger.error("Failed to find new parent item of %s" % tHandle) @@ -609,8 +612,8 @@ class GuiDocTree(QTreeWidget): def _moveOrphanedItem(self, tHandle, dHandle): trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.getItem(tHandle) - nwItemD = self.theProject.getItem(dHandle) + nwItemS = self.theProject.projTree[tHandle] + nwItemD = self.theProject.projTree[dHandle] trItemP = trItemS.parent() nwItemS.setClass(nwItemD.itemClass) if trItemP is None: @@ -652,8 +655,8 @@ class GuiDocTree(QTreeWidget): dItem = self.itemFromIndex(dIndex) dHandle = dItem.text(self.C_HANDLE) - snItem = self.theProject.getItem(sHandle) - dnItem = self.theProject.getItem(dHandle) + snItem = self.theProject.projTree[sHandle] + dnItem = self.theProject.projTree[dHandle] if dnItem is None: self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) return diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index d04aa152..af51e908 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -123,7 +123,7 @@ class GuiDocViewer(QTextBrowser): """Load text into the viewer from an item handle. """ - tItem = self.theProject.getItem(tHandle) + tItem = self.theProject.projTree[tHandle] if tItem is None: logger.warning("Item not found") return False diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 660af8b3..54cfbf5a 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -405,7 +405,7 @@ class GuiProjectOutline(QTreeWidget): """Populate a tree item with all the column values. """ - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] novIdx = self.theIndex.novelIndex[tHandle][sTitle] newItem = QTreeWidgetItem() diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index 9a12293f..cb0d6af3 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -103,7 +103,7 @@ class GuiDocViewDetails(QWidget): theRefs = self.theParent.theIndex.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: - tItem = self.theProject.getItem(tHandle) + tItem = self.theProject.projTree[tHandle] if tItem is not None: theList.append("%s" % (tHandle,tItem.itemName)) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index e6553191..f1021fc3 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -68,7 +68,7 @@ class GuiMainMenu(QMenuBar): if itemClass == nwItemClass.NO_CLASS: continue if itemClass == nwItemClass.TRASH: continue self.rootItems[itemClass].setEnabled( - self.theProject.checkRootUnique(itemClass) + self.theProject.projTree.checkRootUnique(itemClass) ) return diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index cfdad370..fbe3a02e 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -228,7 +228,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): return if theText.startswith("@"): # Keywords and commands - tItem = self.theParent.theProject.getItem(self.theHandle) + tItem = self.theParent.theProject.projTree[self.theHandle] isValid, theBits, thePos = self.theIndex.scanThis(theText) isGood = self.theIndex.checkThese(theBits, tItem) if isValid: diff --git a/nw/guimain.py b/nw/guimain.py index fb221ba2..14e0816b 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -45,7 +45,7 @@ from nw.gui import ( GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline, GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad ) -from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup +from nw.project import NWProject, NWDoc, NWIndex, NWBackup from nw.tools import countWords from nw.constants import nwFiles, nwItemType, nwAlert @@ -617,7 +617,7 @@ class GuiMain(QMainWindow): return False logger.verbose("Opening item %s" % tHandle) - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) self.openDocument(tHandle) @@ -656,7 +656,7 @@ class GuiMain(QMainWindow): self.treeView.saveTreeOrder() self.theIndex.clearIndex() - nItems = len(self.theProject.treeOrder) + nItems = len(self.theProject.projTree) dlgProg = QProgressDialog("Scanning files ...", "Cancel", 0, nItems, self) dlgProg.setWindowModality(Qt.WindowModal) @@ -668,27 +668,27 @@ class GuiMain(QMainWindow): time.sleep(0.5) nDone = 0 - for tHandle in self.theProject.treeOrder: - - tItem = self.theProject.getItem(tHandle) + for tItem in self.theProject.projTree: dlgProg.setValue(nDone) - dlgProg.setLabelText("Scanning: %s" % tItem.itemName) - logger.verbose("Scanning: %s" % tItem.itemName) if tItem is not None and tItem.itemType == nwItemType.FILE: + + dlgProg.setLabelText("Scanning: %s" % tItem.itemName) + logger.verbose("Scanning: %s" % tItem.itemName) + theDoc = NWDoc(self.theProject, self) - theText = theDoc.openDocument(tHandle, False) + theText = theDoc.openDocument(tItem.itemHandle, False) # Build tag index - self.theIndex.scanText(tHandle, theText) + self.theIndex.scanText(tItem.itemHandle, theText) # Get Word Counts - cC, wC, pC = self.theIndex.getCounts(tHandle) + cC, wC, pC = self.theIndex.getCounts(tItem.itemHandle) tItem.setCharCount(cC) tItem.setWordCount(wC) tItem.setParaCount(pC) - self.treeView.propagateCount(tHandle, wC) + self.treeView.propagateCount(tItem.itemHandle, wC) self.treeView.projectWordCount() nDone += 1 @@ -1020,7 +1020,7 @@ class GuiMain(QMainWindow): def _treeDoubleClick(self, tItem, colNo): tHandle = tItem.text(3) logger.verbose("User double clicked tree item with handle %s" % tHandle) - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) self.openDocument(tHandle) @@ -1031,7 +1031,7 @@ class GuiMain(QMainWindow): def _treeKeyPressReturn(self): tHandle = self.treeView.getSelectedHandle() logger.verbose("User pressed return on tree item with handle %s" % tHandle) - nwItem = self.theProject.getItem(tHandle) + nwItem = self.theProject.projTree[tHandle] if nwItem.itemType == nwItemType.FILE: logger.verbose("Requested item %s is a file" % tHandle) self.openDocument(tHandle) diff --git a/nw/project/__init__.py b/nw/project/__init__.py index d7065c78..b36f5a5e 100644 --- a/nw/project/__init__.py +++ b/nw/project/__init__.py @@ -3,15 +3,11 @@ from nw.project.backup import NWBackup from nw.project.document import NWDoc from nw.project.index import NWIndex -from nw.project.item import NWItem from nw.project.project import NWProject -from nw.project.status import NWStatus __all__ = [ "NWBackup", "NWDoc", "NWIndex", - "NWItem", "NWProject", - "NWStatus", ] diff --git a/nw/project/document.py b/nw/project/document.py index b456043c..1e200d33 100644 --- a/nw/project/document.py +++ b/nw/project/document.py @@ -62,7 +62,7 @@ class NWDoc(): def openDocument(self, tHandle, showStatus=True): self.docHandle = tHandle - self.theItem = self.theProject.getItem(tHandle) + self.theItem = self.theProject.projTree[tHandle] if self.theItem is None: self.clearDocument() @@ -71,7 +71,7 @@ class NWDoc(): # By default, the document is editable. # Except for files in the trash folder. self.docEditable = True - if self.theItem.parHandle == self.theProject.trashRoot: + if self.theItem.parHandle == self.theProject.projTree.trashRoot(): self.docEditable = False docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN) diff --git a/nw/project/index.py b/nw/project/index.py index 7ff301a7..9cc1098b 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -249,12 +249,12 @@ class NWIndex(): files before we save them, unless we're rebuilding the index. """ - theItem = self.theProject.getItem(tHandle) + theItem = self.theProject.projTree[tHandle] if theItem is None: return False if theItem.itemType != nwItemType.FILE: return False - if theItem.parHandle == self.theProject.trashRoot: + if theItem.parHandle == self.theProject.projTree.trashRoot(): return False if theItem.itemLayout == nwItemLayout.NO_LAYOUT: return False @@ -539,7 +539,7 @@ class NWIndex(): """ theStructure = [] - for tHandle in self.theProject.treeOrder: + for tHandle in self.theProject.projTree.handles(): if tHandle not in self.novelIndex: continue for sTitle in sorted(self.novelIndex[tHandle].keys()): @@ -605,7 +605,7 @@ class NWIndex(): theRefs = {} - tItem = self.theProject.getItem(tHandle) + tItem = self.theProject.projTree[tHandle] if tHandle is None: return theRefs diff --git a/nw/project/item.py b/nw/project/item.py deleted file mode 100644 index 7843270a..00000000 --- a/nw/project/item.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Project Item - - novelWriter – Project Item -============================ - Class holding a project 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 . -""" - -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 - - # Document Meta Data - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 - self.cursorPos = 0 - - return - - ## - # XML Pack - ## - - def packXML(self, xParent): - 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)) - xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) - if self.itemType == nwItemType.FILE: - 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) - return xPack - - def _subPack(self, xParent, name, attrib=None, text=None, none=True): - 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 - - ## - # Settings Wrapper - ## - - def setFromTag(self, tagName, tagValue): - logger.verbose("Setting tag '%s' to value '%s'" % (tagName, str(tagValue))) - if tagName == "name": - self.setName(tagValue) - elif tagName == "order": - self.setOrder(tagValue) - elif tagName == "type": - self.setType(tagValue) - elif tagName == "class": - self.setClass(tagValue) - elif tagName == "layout": - self.setLayout(tagValue) - elif tagName == "status": - self.setStatus(tagValue) - elif tagName == "expanded": - self.setExpanded(tagValue) - elif tagName == "charCount": - self.setCharCount(tagValue) - elif tagName == "wordCount": - self.setWordCount(tagValue) - elif tagName == "paraCount": - self.setParaCount(tagValue) - elif tagName == "cursorPos": - self.setCursorPos(tagValue) - else: - logger.error("Unknown tag '%s'" % tagName) - return - - ## - # Set Item Values - ## - - def setName(self, theName): - self.itemName = theName.strip() - return - - def setHandle(self, theHandle): - self.itemHandle = theHandle - return - - def setParent(self, theParent): - self.parHandle = theParent - return - - def setOrder(self, theOrder): - self.itemOrder = theOrder - return - - def setType(self, theType): - if isinstance(theType, nwItemType): - self.itemType = theType - return - else: - for itemType in nwItemType: - if theType == itemType.name: - self.itemType = itemType - return - logger.error("Unrecognised item type '%s'" % theType) - self.itemType = nwItemType.NO_TYPE - return - - def setClass(self, theClass): - if isinstance(theClass, nwItemClass): - self.itemClass = theClass - return - else: - for itemClass in nwItemClass: - if theClass == itemClass.name: - self.itemClass = itemClass - return - logger.error("Unrecognised item class '%s'" % theClass) - self.itemClass = nwItemClass.NO_CLASS - return - - def setLayout(self, theLayout): - if isinstance(theLayout, nwItemLayout): - self.itemLayout = theLayout - return - else: - for itemLayout in nwItemLayout: - if theLayout == itemLayout.name: - self.itemLayout = itemLayout - return - logger.error("Unrecognised item layout '%s'" % theLayout) - self.itemLayout = nwItemLayout.NO_LAYOUT - return - - def setStatus(self, theStatus): - 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): - if isinstance(expState, str): - self.isExpanded = expState == str(True) - else: - self.isExpanded = expState - return - - ## - # Set Document Meta Data - ## - - def setCharCount(self, theCount): - theCount = checkInt(theCount,0) - self.charCount = theCount - return - - def setWordCount(self, theCount): - theCount = checkInt(theCount,0) - self.wordCount = theCount - return - - def setParaCount(self, theCount): - theCount = checkInt(theCount,0) - self.paraCount = theCount - return - - def setCursorPos(self, thePosition): - thePosition = checkInt(thePosition,0) - self.cursorPos = thePosition - return - -# END Class NWItem diff --git a/nw/project/project.py b/nw/project/project.py index 45294564..cd3fdc55 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -6,7 +6,12 @@ Class holding a project File History: - Created: 2018-09-29 [0.0.1] + Created: 2018-09-29 [0.0.1] NWProject + Added: 2018-10-27 [0.0.1] NWItem + Added: 2019-05-19 [0.1.3] NWStatus + 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 This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -34,8 +39,6 @@ from hashlib import sha256 from datetime import datetime from time import time -from nw.project.status import NWStatus -from nw.project.item import NWItem from nw.tools import projectMaintenance, OptionState from nw.common import checkString, checkBool, checkInt from nw.constants import ( @@ -49,9 +52,14 @@ class NWProject(): def __init__(self, theParent): # Internal - self.theParent = theParent - self.mainConf = self.theParent.mainConf - self.optState = OptionState(self) + self.theParent = theParent + self.mainConf = nw.CONFIG + + # Core Elements + self.optState = OptionState(self) # Project-specific GUI options + self.projTree = NWTree(self) # The project tree + + # Project Status self.projOpened = None # The time stamp of when the project file was opened self.projChanged = None # The project has unsaved changes self.projAltered = None # The project has been altered this session @@ -59,18 +67,11 @@ class NWProject(): self.saveCount = None # Meta data: number of saves self.autoCount = None # Meta data: number of automatic saves - # Debug - self.handleSeed = None - # Class Settings - self.projTree = None # Holds all the items of the project - self.treeOrder = None # The order of the tree items on the tree view - self.treeRoots = None # The root items of the tree - self.trashRoot = None # The handle of the trash root folder - self.projPath = None # The full path to where the currently open project is saved - self.projMeta = None # The full path to the project's meta data folder - self.projDict = None # The spell check dictionary - self.projFile = None # The file name of the project main xml file + self.projPath = None # The full path to where the currently open project is saved + self.projMeta = None # The full path to the project's meta data folder + self.projDict = None # The spell check dictionary + self.projFile = None # The file name of the project main XML file # Project Meta self.projName = None @@ -104,7 +105,10 @@ class NWProject(): ## def newRoot(self, rootName, rootClass): - if not self.checkRootUnique(rootClass): + """Add a new root item. These items are unique, except for item class + CUSTOM, and always have parent handle set to None. + """ + if not self.projTree.checkRootUnique(rootClass): self.makeAlert("Duplicate root item detected!", nwAlert.ERROR) return None newItem = NWItem(self) @@ -112,19 +116,24 @@ class NWProject(): newItem.setType(nwItemType.ROOT) newItem.setClass(rootClass) newItem.setStatus(0) - self._appendItem(None,None,newItem) + self.projTree.append(None, None, newItem) return newItem.itemHandle def newFolder(self, folderName, folderClass, pHandle): + """Add a new folder with a given name and class and parent item. + """ newItem = NWItem(self) newItem.setName(folderName) newItem.setType(nwItemType.FOLDER) newItem.setClass(folderClass) newItem.setStatus(0) - self._appendItem(None,pHandle,newItem) + self.projTree.append(None, pHandle, newItem) return newItem.itemHandle def newFile(self, fileName, fileClass, pHandle): + """Add a new file with a given name and class, and set a default + layout based on the class. SCENE for NOVEL, and otherwise NOTE. + """ newItem = NWItem(self) newItem.setName(fileName) newItem.setType(nwItemType.FILE) @@ -134,15 +143,17 @@ class NWProject(): newItem.setLayout(nwItemLayout.NOTE) newItem.setClass(fileClass) newItem.setStatus(0) - self._appendItem(None,pHandle,newItem) + self.projTree.append(None, pHandle, newItem) return newItem.itemHandle def addTrash(self): + """Add the special trash root folder to the project. + """ newItem = NWItem(self) newItem.setName("Trash") newItem.setType(nwItemType.TRASH) newItem.setClass(nwItemClass.TRASH) - self._appendItem(None,None,newItem) + self.projTree.append(None, None, newItem) return newItem.itemHandle ## @@ -150,6 +161,9 @@ class NWProject(): ## def newProject(self): + """Create a new project by populating the project tree with a + few starter items. + """ hNovel = self.newRoot("Novel", nwItemClass.NOVEL) hChars = self.newRoot("Characters", nwItemClass.CHARACTER) hWorld = self.newRoot("Plot", nwItemClass.PLOT) @@ -165,17 +179,17 @@ class NWProject(): default values. """ + # Project Status self.projOpened = None self.projChanged = None self.projAltered = False self.saveCount = 0 self.autoCount = 0 + # Project Tree + self.projTree.clear() + # Project Settings - self.projTree = {} - self.treeOrder = [] - self.treeRoots = [] - self.trashRoot = None self.projPath = None self.projMeta = None self.projDict = None @@ -334,20 +348,9 @@ class NWProject(): elif xChild.tag == "content": logger.debug("Found project content") for xItem in xChild: - itemAttrib = xItem.attrib - if "handle" in xItem.attrib: - tHandle = itemAttrib["handle"] - else: - logger.error("Skipping entry missing handle") - continue - if "parent" in xItem.attrib: - pHandle = itemAttrib["parent"] - else: - pHandle = None nwItem = NWItem(self) - for xValue in xItem: - nwItem.setFromTag(xValue.tag,xValue.text) - self._appendItem(tHandle,pHandle,nwItem) + if nwItem.unpackXML(xItem): + self.projTree.append(nwItem.itemHandle, nwItem.parHandle, nwItem) self.optState.loadSettings() @@ -425,9 +428,9 @@ class NWProject(): # Save Tree Content logger.debug("Writing project content") - xContent = etree.SubElement(nwXML, "content", attrib={"count":str(len(self.treeOrder))}) - for tHandle in self.treeOrder: - self.projTree[tHandle].packXML(xContent) + xContent = etree.SubElement(nwXML, "content", attrib={"count":str(len(self.projTree))}) + for tItem in self.projTree: + tItem.packXML(xContent) # Write the xml tree to file tempFile = path.join(self.projPath, self.projFile+"~") @@ -476,7 +479,7 @@ class NWProject(): return True ## - # Set Functions + # Setters ## def setProjectPath(self, projPath): @@ -540,9 +543,9 @@ class NWProject(): return True def setTreeOrder(self, newOrder): - if len(self.treeOrder) != len(newOrder): + if len(self.projTree) != len(newOrder): logger.warning("Size of new and old tree order does not match") - self.treeOrder = newOrder + self.projTree.setOrder(newOrder) self.setProjectChanged(True) return True @@ -566,8 +569,8 @@ class NWProject(): def setStatusColours(self, newCols): replaceMap = self.statusItems.setNewEntries(newCols) - if self.projTree is not None: - for nwItem in self.projTree.values(): + if self.projTree: + for nwItem in self.projTree: if nwItem.itemClass == nwItemClass.NOVEL: if nwItem.itemStatus in replaceMap.keys(): nwItem.setStatus(replaceMap[nwItem.itemStatus]) @@ -576,8 +579,8 @@ class NWProject(): def setImportColours(self, newCols): replaceMap = self.importItems.setNewEntries(newCols) - if self.projTree is not None: - for nwItem in self.projTree.values(): + if self.projTree: + for nwItem in self.projTree: if nwItem.itemClass != nwItemClass.NOVEL: if nwItem.itemStatus in replaceMap.keys(): nwItem.setStatus(replaceMap[nwItem.itemStatus]) @@ -597,73 +600,29 @@ class NWProject(): return self.projChanged ## - # Get Functions + # Getters ## - 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 getSessionWordCount(self): + """Returns the number of words added or removed this session. + """ return self.currWCount - self.lastWCount - 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 - def getProjectItems(self): - """This function is called from the tree view when building the - tree. Each item in the project is returned in the order saved in - the project file, but first it checks that it has a parent item - already sent to the tree. + """This function ensures that the item tree loaded is sent to + the GUI tree view in such a way that the tree can be built. That + is, the parent item must be sent before its child. In principle, + a proper XML file will already ensure that, but in the event the + order has been altered, or a file is orphaned, this function is + capable of handling it. """ sentItems = [] - iterItems = self.treeOrder.copy() - n = 0 + iterItems = self.projTree.handles() + n = 0 nMax = len(iterItems) while n < nMax: tHandle = iterItems[n] - tItem = self.getItem(tHandle) + tItem = self.projTree[tHandle] n += 1 if n > 10000: return # Just in case @@ -696,43 +655,13 @@ class NWProject(): # Class Methods ## - def deleteItem(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 treeOrder as it does not exist" % tHandle - ) - return False - self.treeOrder.remove(tHandle) - self.setProjectChanged(True) - return True - - def findRootItem(self, theClass): - for aRoot in self.treeRoots: - if theClass == self.projTree[aRoot].itemClass: - return self.projTree[aRoot].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. - """ - if theClass == nwItemClass.CUSTOM: - return True - for aRoot in self.treeRoots: - if theClass == self.projTree[aRoot].itemClass: - return False - return True - def countStatus(self): """Count how many times the various status flags are used in the project tree. """ self.statusItems.resetCounts() self.importItems.resetCounts() - for nwItem in self.projTree.values(): + for nwItem in self.projTree: if nwItem.itemClass == nwItemClass.NOVEL: self.statusItems.countEntry(nwItem.itemStatus) else: @@ -832,6 +761,11 @@ class NWProject(): return def _scanProjectFolder(self): + """Scan the project folder and check that the files in it are + also in the project CML file. If they aren't, import them as + orphaned files so the user can either delete them, or put them + back into the project tree. + """ if self.projPath is None: return @@ -855,7 +789,7 @@ class NWProject(): logger.warning("Skipping file %s" % fileItem) continue fHandle = fileItem[5]+fileItem[7:19] - if fHandle in self.treeOrder: + if fHandle in self.projTree: logger.debug("Checking file %s, handle %s: OK" % (fileItem,fHandle)) else: logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem,fHandle)) @@ -875,42 +809,18 @@ class NWProject(): nOrph = 0 for oHandle in orphanFiles: nOrph += 1 - orItem = NWItem(self) - orItem.setName("Orphaned File %d" % nOrph) - orItem.setType(nwItemType.FILE) - orItem.setClass(nwItemClass.NO_CLASS) - orItem.setLayout(nwItemLayout.NO_LAYOUT) - self._appendItem(oHandle,None,orItem) - - return - - def _appendItem(self, tHandle, pHandle, nwItem): - tHandle = checkString(tHandle,self._makeHandle(),False) - pHandle = checkString(pHandle,None,True) - 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.setProjectChanged(True) + orphItem = NWItem(self) + orphItem.setName("Orphaned File %d" % nOrph) + orphItem.setType(nwItemType.FILE) + orphItem.setClass(nwItemClass.NO_CLASS) + orphItem.setLayout(nwItemLayout.NO_LAYOUT) + self.projTree.append(oHandle, None, orphItem) return def _appendSessionStats(self): + """Append session statistics to the sessions log file. + """ if self.projMeta is None: return False @@ -930,18 +840,633 @@ class NWProject(): return True +# 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 = "" # 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 = "" + 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 + + ## + # 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. + """ + 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) + + 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 + + ## + # Meta Methods + ## + + def __len__(self): + return self._theLength + + def __bool__(self): + 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=""): - if self.handleSeed is None: + """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 + 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.keys(): + if itemHandle in self._projTree: logger.warning("Duplicate handle encountered! Retrying ...") itemHandle = self._makeHandle(addSeed+"!") return itemHandle -# END Class NWProject +# 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 + + # 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)) + xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) + if self.itemType == nwItemType.FILE: + 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) + 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, + "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): + 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): + self.itemName = theName.strip() + return + + def setHandle(self, theHandle): + if isinstance(theHandle, str): + if len(theHandle) == 13: + self.itemHandle = theHandle + else: + self.itemHandle = None + else: + self.itemHandle = None + return + + def setParent(self, theParent): + 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): + self.itemOrder = checkInt(theOrder, 0) + return + + def setType(self, theType): + 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): + 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): + 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): + 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): + if isinstance(expState, str): + self.isExpanded = expState == str(True) + else: + self.isExpanded = expState == True + return + + ## + # Set Document Meta Data + ## + + def setCharCount(self, theCount): + self.charCount = checkInt(theCount,0) + return + + def setWordCount(self, theCount): + self.wordCount = checkInt(theCount,0) + return + + def setParaCount(self, theCount): + self.paraCount = checkInt(theCount,0) + return + + def setCursorPos(self, thePosition): + self.cursorPos = checkInt(thePosition,0) + return + +# END Class NWItem + +# ================================================================================================ # +# 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): + 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): + 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): + 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): + + 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): + self.theCounts = [0]*self.theLength + return + + def countEntry(self, theLabel): + 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): + if n >= 0 and n < self.theLength: + return self.theLabels[n], self.theColours[n], self.theCounts[n] + return None, None, None + + def __iter__(self): + self.theIndex = 0 + return self + + def __next__(self): + 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/project/status.py b/nw/project/status.py deleted file mode 100644 index c7771f3d..00000000 --- a/nw/project/status.py +++ /dev/null @@ -1,170 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Item Status - - novelWriter – Item Status -=========================== - Class holding the project's item statuses - - 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): - 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): - 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): - 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): - - 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): - self.theCounts = [0]*self.theLength - return - - def countEntry(self, theLabel): - theIndex = self.lookupEntry(theLabel) - if theIndex is not None: - self.theCounts[theIndex] += 1 - return - - def packEntries(self, xParent): - 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): - - 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): - if n >= 0 and n < self.theLength: - return self.theLabels[n], self.theColours[n], self.theCounts[n] - return None, None, None - - def __iter__(self): - self.theIndex = 0 - return self - - def __next__(self): - 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/tests/reference/gui/1_fca346db6561_main.nwd b/tests/reference/gui/1_8010bd9270f9_main.nwd similarity index 100% rename from tests/reference/gui/1_fca346db6561_main.nwd rename to tests/reference/gui/1_8010bd9270f9_main.nwd diff --git a/tests/reference/gui/1_688b6ef52555_main.nwd b/tests/reference/gui/1_a6562590ef19_main.nwd similarity index 100% rename from tests/reference/gui/1_688b6ef52555_main.nwd rename to tests/reference/gui/1_a6562590ef19_main.nwd diff --git a/tests/reference/gui/1_2d20bbd7e394_main.nwd b/tests/reference/gui/1_e17daca5f3e1_main.nwd similarity index 100% rename from tests/reference/gui/1_2d20bbd7e394_main.nwd rename to tests/reference/gui/1_e17daca5f3e1_main.nwd diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 02119a65..c55ff681 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -59,7 +59,7 @@ New True - + New File FILE CHARACTER @@ -78,7 +78,7 @@ New True - + New File FILE PLOT @@ -97,7 +97,7 @@ New True - + New File FILE WORLD diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index a40a2e8d..92bd9df4 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -73,28 +73,28 @@ 0 0 - + Timeline ROOT TIMELINE New False - + Object ROOT OBJECT New False - + Custom1 ROOT CUSTOM New False - + Custom2 ROOT CUSTOM diff --git a/tests/test_gui.py b/tests/test_gui.py index c8f8d013..e2291fff 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -25,15 +25,15 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) # Create new, save, close project - nwGUI.theProject.handleSeed = 42 + nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject(nwTempGUI, True) assert nwGUI.saveProject() assert nwGUI.closeProject() assert len(nwGUI.theProject.projTree) == 0 - assert len(nwGUI.theProject.treeOrder) == 0 - assert len(nwGUI.theProject.treeRoots) == 0 - assert nwGUI.theProject.trashRoot is None + assert len(nwGUI.theProject.projTree._treeOrder) == 0 + assert len(nwGUI.theProject.projTree._treeRoots) == 0 + assert nwGUI.theProject.projTree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None assert nwGUI.theProject.projFile == "nwProject.nwx" @@ -55,9 +55,9 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): # Check that we loaded the data assert len(nwGUI.theProject.projTree) == 6 - assert len(nwGUI.theProject.treeOrder) == 6 - assert len(nwGUI.theProject.treeRoots) == 4 - assert nwGUI.theProject.trashRoot is None + assert len(nwGUI.theProject.projTree._treeOrder) == 6 + assert len(nwGUI.theProject.projTree._treeRoots) == 4 + assert nwGUI.theProject.projTree.trashRoot() is None assert nwGUI.theProject.projPath == nwTempGUI assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta") assert nwGUI.theProject.projFile == "nwProject.nwx" @@ -237,14 +237,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): # Check the files refFile = path.join(nwTempGUI,"nwProject.nwx") assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) - refFile = path.join(nwTempGUI,"data_0","2d20bbd7e394_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_2d20bbd7e394_main.nwd")) - refFile = path.join(nwTempGUI,"data_2","fca346db6561_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_fca346db6561_main.nwd")) + refFile = path.join(nwTempGUI,"data_0","e17daca5f3e1_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_e17daca5f3e1_main.nwd")) + refFile = path.join(nwTempGUI,"data_9","8010bd9270f9_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_8010bd9270f9_main.nwd")) refFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) - refFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd")) + refFile = path.join(nwTempGUI,"data_1","a6562590ef19_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_a6562590ef19_main.nwd")) nwGUI.closeMain() # qtbot.stopForInteraction() @@ -258,7 +258,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) # Create new, save, open project - nwGUI.theProject.handleSeed = 42 + nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject(nwTempGUI, True) nwGUI.mainConf.backupPath = nwTempGUI @@ -345,7 +345,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) # Create new, save, open project - nwGUI.theProject.handleSeed = 42 + nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject(nwTempGUI, True) itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") diff --git a/tests/test_item.py b/tests/test_item.py new file mode 100644 index 00000000..b58f1b7e --- /dev/null +++ b/tests/test_item.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +"""novelWriter NWItem Class Tester +""" + +import nw +import pytest + +from lxml import etree +from nwdummy import DummyMain + +from nw.config import Config +from nw.project.project import NWProject, NWItem +from nw.constants import nwItemClass, nwItemType, nwItemLayout + +theConf = Config() +theMain = DummyMain() +theMain.mainConf = theConf + +theProject = NWProject(theMain) +theItem = NWItem(theProject) +nwXML = etree.Element("novelWriterXML") + +@pytest.mark.project +def testItemSettersSimple(): + + # Name + theItem.setName("A Name") + assert theItem.itemName == "A Name" + theItem.setName("\t A Name ") + assert theItem.itemName == "A Name" + + # Handle + theItem.setHandle(123) + assert theItem.itemHandle is None + theItem.setHandle("0123456789abcdef") + assert theItem.itemHandle is None + theItem.setHandle("0123456789abc") + assert theItem.itemHandle == "0123456789abc" + + # Parent + theItem.setParent(None) + assert theItem.parHandle is None + theItem.setParent(123) + assert theItem.parHandle is None + theItem.setParent("0123456789abcdef") + assert theItem.parHandle is None + theItem.setParent("0123456789abc") + assert theItem.parHandle == "0123456789abc" + + # Order + theItem.setOrder(None) + assert theItem.itemOrder == 0 + theItem.setOrder("1") + assert theItem.itemOrder == 1 + theItem.setOrder(1) + assert theItem.itemOrder == 1 + + # Status + theItem.setStatus("Nonsense") + assert theItem.itemStatus == "New" + theItem.setStatus("New") + assert theItem.itemStatus == "New" + theItem.setStatus("Minor") + assert theItem.itemStatus == "Minor" + theItem.setStatus("Major") + assert theItem.itemStatus == "Major" + theItem.setStatus("Main") + assert theItem.itemStatus == "Main" + + # Expanded + theItem.setExpanded(8) + assert not theItem.isExpanded + theItem.setExpanded(None) + assert not theItem.isExpanded + theItem.setExpanded("None") + assert not theItem.isExpanded + theItem.setExpanded("What?") + assert not theItem.isExpanded + theItem.setExpanded("True") + assert theItem.isExpanded + theItem.setExpanded(True) + assert theItem.isExpanded + + # CharCount + theItem.setCharCount(None) + assert theItem.charCount == 0 + theItem.setCharCount("1") + assert theItem.charCount == 1 + theItem.setCharCount(1) + assert theItem.charCount == 1 + + # WordCount + theItem.setWordCount(None) + assert theItem.wordCount == 0 + theItem.setWordCount("1") + assert theItem.wordCount == 1 + theItem.setWordCount(1) + assert theItem.wordCount == 1 + + # ParaCount + theItem.setParaCount(None) + assert theItem.paraCount == 0 + theItem.setParaCount("1") + assert theItem.paraCount == 1 + theItem.setParaCount(1) + assert theItem.paraCount == 1 + + # CursorPos + theItem.setCursorPos(None) + assert theItem.cursorPos == 0 + theItem.setCursorPos("1") + assert theItem.cursorPos == 1 + theItem.setCursorPos(1) + assert theItem.cursorPos == 1 + +@pytest.mark.project +def testItemClassSetter(): + + # Class + theItem.setClass(None) + assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NONSENSE") + assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NO_CLASS") + assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NOVEL") + assert theItem.itemClass == nwItemClass.NOVEL + theItem.setClass("PLOT") + assert theItem.itemClass == nwItemClass.PLOT + theItem.setClass("CHARACTER") + assert theItem.itemClass == nwItemClass.CHARACTER + theItem.setClass("WORLD") + assert theItem.itemClass == nwItemClass.WORLD + theItem.setClass("TIMELINE") + assert theItem.itemClass == nwItemClass.TIMELINE + theItem.setClass("OBJECT") + assert theItem.itemClass == nwItemClass.OBJECT + theItem.setClass("ENTITY") + assert theItem.itemClass == nwItemClass.ENTITY + theItem.setClass("CUSTOM") + assert theItem.itemClass == nwItemClass.CUSTOM + theItem.setClass("TRASH") + assert theItem.itemClass == nwItemClass.TRASH + +@pytest.mark.project +def testItemTypeSetter(): + + # Class + theItem.setType(None) + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("NONSENSE") + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("NO_TYPE") + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("ROOT") + assert theItem.itemType == nwItemType.ROOT + theItem.setType("FOLDER") + assert theItem.itemType == nwItemType.FOLDER + theItem.setType("FILE") + assert theItem.itemType == nwItemType.FILE + theItem.setType("TRASH") + assert theItem.itemType == nwItemType.TRASH + +@pytest.mark.project +def testItemLayoutSetter(): + + # Class + theItem.setLayout(None) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + theItem.setLayout("NONSENSE") + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + theItem.setLayout("NO_LAYOUT") + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + theItem.setLayout("TITLE") + assert theItem.itemLayout == nwItemLayout.TITLE + theItem.setLayout("BOOK") + assert theItem.itemLayout == nwItemLayout.BOOK + theItem.setLayout("PAGE") + assert theItem.itemLayout == nwItemLayout.PAGE + theItem.setLayout("PARTITION") + assert theItem.itemLayout == nwItemLayout.PARTITION + theItem.setLayout("UNNUMBERED") + assert theItem.itemLayout == nwItemLayout.UNNUMBERED + theItem.setLayout("CHAPTER") + assert theItem.itemLayout == nwItemLayout.CHAPTER + theItem.setLayout("SCENE") + assert theItem.itemLayout == nwItemLayout.SCENE + theItem.setLayout("NOTE") + assert theItem.itemLayout == nwItemLayout.NOTE + +@pytest.mark.project +def testItemXMLPackUnpack(): + + # Pack + xContent = etree.SubElement(nwXML, "content") + theItem.packXML(xContent) + assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( + b"" + b"" + b"A NameTRASHTRASHMainTrue" + b"" + b"" + ) + + # Unpack + assert theItem.unpackXML(xContent[0]) + assert theItem.itemHandle == "0123456789abc" + assert theItem.parHandle == "0123456789abc" + assert theItem.itemOrder == 1 + assert theItem.isExpanded + assert theItem.charCount == 1 + assert theItem.wordCount == 1 + assert theItem.paraCount == 1 + assert theItem.cursorPos == 1 + assert theItem.itemClass == nwItemClass.TRASH + assert theItem.itemType == nwItemType.TRASH + assert theItem.itemLayout == nwItemLayout.NOTE diff --git a/tests/test_project.py b/tests/test_project.py index a38d6d58..c029fbba 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2,24 +2,24 @@ """novelWriter Project Class Tester """ -import nw, pytest, types +import nw +import pytest from os import path from nwtools import * from nwdummy import DummyMain -from nw.config import Config +from nw.config import Config from nw.project.project import NWProject -from nw.project.item import NWItem -from nw.project.index import NWIndex -from nw.constants import nwItemClass +from nw.project.index import NWIndex +from nw.constants import nwItemClass theConf = Config() theMain = DummyMain() theMain.mainConf = theConf theProject = NWProject(theMain) -theProject.handleSeed = 42 +theProject.projTree.setSeed(42) @pytest.mark.project def testProjectNew(nwTempProj,nwRef,nwTemp):