Allow multiple roots of same class (#1031)

This commit is contained in:
Veronica Berglyd Olsen
2022-04-17 19:35:51 +02:00
committed by GitHub
38 changed files with 1230 additions and 906 deletions
+1 -20
View File
@@ -25,7 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline
from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline
def trConst(tString):
@@ -42,31 +42,12 @@ class nwConst():
FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# END Class nwConst
class nwLists():
"""Lists used for grouping various other constants.
"""
# Regular user-accessible item types
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# Item classes where the full list of novel layouts are allowed
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# Deprecated nwItemLayout entries
DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE")
# END Class nwLists
class nwRegEx():
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
+3 -8
View File
@@ -30,7 +30,7 @@ import logging
from time import time
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode
from novelwriter.core.document import NWDoc
@@ -208,8 +208,6 @@ class NWIndex():
text.
"""
theItem = self.theProject.projTree[tHandle]
theRoot = self.theProject.projTree.getRootItem(tHandle)
if theItem is None:
logger.info("Not indexing unknown item '%s'", tHandle)
return False
@@ -229,11 +227,8 @@ class NWIndex():
if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
logger.debug("Not indexing trash item '%s'", tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.debug("Not indexing archived item '%s'", tHandle)
if theItem.isInactive():
logger.debug("Not indexing inactive item '%s'", tHandle)
return False
itemClass = theItem.itemClass
+100 -37
View File
@@ -31,7 +31,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, nwLists, trConst
from novelwriter.constants import nwLabels, trConst
logger = logging.getLogger(__name__)
@@ -45,6 +45,7 @@ class NWItem():
self._name = ""
self._handle = None
self._parent = None
self._root = None
self._order = 0
self._type = nwItemType.NO_TYPE
self._class = nwItemClass.NO_CLASS
@@ -85,6 +86,10 @@ class NWItem():
def itemParent(self):
return self._parent
@property
def itemRoot(self):
return self._root
@property
def itemOrder(self):
return self._order
@@ -147,6 +152,7 @@ class NWItem():
itemAttrib = {}
itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name)
@@ -188,6 +194,7 @@ class NWItem():
return False
self.setParent(xItem.attrib.get("parent", None))
self.setRoot(xItem.attrib.get("root", None))
self.setOrder(xItem.attrib.get("order", 0))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
@@ -249,7 +256,7 @@ class NWItem():
return
##
# Methods
# Lookup Methods
##
def describeMe(self, hLevel=None):
@@ -275,11 +282,26 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def isNovelLike(self):
"""Returns true if the item is of a novel-like class.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
def documentAllowed(self):
"""Returns true if the item is allowed to be of document layout.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def isInactive(self):
"""Returns true if the item is in an inactive class.
"""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def getImportStatus(self):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self._class in nwLists.CLS_NOVEL:
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
else:
@@ -287,14 +309,44 @@ class NWItem():
stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
def setImportStatus(self, theLabel):
##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport.
"""
if self._class in nwLists.CLS_NOVEL:
self.setStatus(theLabel)
if self.isNovelLike():
self.setStatus(value)
else:
self.setImport(theLabel)
self.setImport(value)
return
def setClassDefaults(self, itemClass):
"""Set the default values based on the item's class and the
project settings.
"""
if self._parent is not None:
# Only update for child items
self.setClass(itemClass)
if self._layout == nwItemLayout.NO_LAYOUT:
# If no layout is set, pick one
if self.isNovelLike():
self._layout = nwItemLayout.DOCUMENT
else:
self._layout = nwItemLayout.NOTE
elif not self.documentAllowed():
# Change layout to note if it is not in an allowed folder
self._layout = nwItemLayout.NOTE
if self._status is None:
self.setStatus("New") # This forces a default value lookup
if self._import is None:
self.setImport("New") # This forces a default value lookup
return
##
@@ -310,26 +362,37 @@ class NWItem():
self._name = ""
return
def setHandle(self, tHandle):
def setHandle(self, handle):
"""Set the item handle, and ensure it is valid.
"""
if isHandle(tHandle):
self._handle = tHandle
if isHandle(handle):
self._handle = handle
else:
self._handle = None
return
def setParent(self, pHandle):
def setParent(self, handle):
"""Set the parent handle, and ensure it is valid.
"""
if pHandle is None:
if handle is None:
self._parent = None
elif isHandle(pHandle):
self._parent = pHandle
elif isHandle(handle):
self._parent = handle
else:
self._parent = None
return
def setRoot(self, handle):
"""Set the root handle, and ensure it is valid.
"""
if handle is None:
self._root = None
elif isHandle(handle):
self._root = handle
else:
self._root = None
return
def setOrder(self, order):
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at
@@ -338,59 +401,59 @@ class NWItem():
self._order = checkInt(order, 0)
return
def setType(self, itemType):
def setType(self, value):
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
if isinstance(itemType, nwItemType):
self._type = itemType
elif isItemType(itemType):
self._type = nwItemType[itemType]
if isinstance(value, nwItemType):
self._type = value
elif isItemType(value):
self._type = nwItemType[value]
else:
logger.error("Unrecognised item type '%s'", itemType)
logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE
return
def setClass(self, itemClass):
def setClass(self, value):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
if isinstance(itemClass, nwItemClass):
self._class = itemClass
elif isItemClass(itemClass):
self._class = nwItemClass[itemClass]
if isinstance(value, nwItemClass):
self._class = value
elif isItemClass(value):
self._class = nwItemClass[value]
else:
logger.error("Unrecognised item class '%s'", itemClass)
logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, itemLayout):
def setLayout(self, value):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
if isinstance(itemLayout, nwItemLayout):
self._layout = itemLayout
elif isItemLayout(itemLayout):
self._layout = nwItemLayout[itemLayout]
elif itemLayout in nwLists.DEP_LAYOUT:
if isinstance(value, nwItemLayout):
self._layout = value
elif isItemLayout(value):
self._layout = nwItemLayout[value]
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", itemLayout)
logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, itemStatus):
def setStatus(self, value):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self.theProject.statusItems.check(itemStatus)
self._status = self.theProject.statusItems.check(value)
return
def setImport(self, itemImport):
def setImport(self, value):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self.theProject.importItems.check(itemImport)
self._import = self.theProject.importItems.check(value)
return
def setExpanded(self, state):
+33 -37
View File
@@ -46,14 +46,14 @@ from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt, simplified
)
from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels
from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
class NWProject():
FILE_VERSION = "1.4"
FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent):
@@ -121,46 +121,34 @@ class NWProject():
##
def newRoot(self, rootName, rootClass):
"""Add a new root item. These items are unique, except for item class
CUSTOM, and always have parent handle set to None.
"""Add a new root item.
"""
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
newItem = NWItem(self)
newItem.setName(rootName)
newItem.setType(nwItemType.ROOT)
newItem.setClass(rootClass)
newItem.setStatus(0)
self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle):
"""Add a new folder with a given name and class and parent item.
def newFolder(self, folderName, pHandle):
"""Add a new folder with a given name and parent item.
"""
newItem = NWItem(self)
newItem.setName(folderName)
newItem.setType(nwItemType.FOLDER)
newItem.setClass(folderClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle):
"""Add a new file with a given name and class, and set a layout
based on the class. DOCUMENT for NOVEL, otherwise NOTE.
def newFile(self, fileName, pHandle):
"""Add a new file with a given name and parent item.
"""
newItem = NWItem(self)
newItem.setName(fileName)
newItem.setType(nwItemType.FILE)
if fileClass == nwItemClass.NOVEL:
newItem.setLayout(nwItemLayout.DOCUMENT)
else:
newItem.setLayout(nwItemLayout.NOTE)
newItem.setClass(fileClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def trashFolder(self):
@@ -173,6 +161,7 @@ class NWProject():
newItem.setType(nwItemType.TRASH)
newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
return trashHandle
@@ -287,10 +276,10 @@ class NWProject():
xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT)
xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER)
xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD)
xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1])
xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1])
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6])
xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1])
xHandle[6] = self.newFolder(self.tr("New Chapter"), xHandle[1])
xHandle[7] = self.newFile(self.tr("New Chapter"), xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), xHandle[6])
aDoc = NWDoc(self, xHandle[5])
aDoc.writeDocument(titlePage)
@@ -313,8 +302,7 @@ class NWProject():
self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot)
# Create a title page
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT)
tHandle = self.newFile(self.tr("Title Page"), nHandle)
aDoc = NWDoc(self, tHandle)
aDoc.writeDocument(titlePage)
@@ -330,10 +318,9 @@ class NWProject():
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
pHandle = nHandle
if chFolders:
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
pHandle = self.newFolder(chTitle, nHandle)
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT)
cHandle = self.newFile(chTitle, pHandle)
aDoc = NWDoc(self, cHandle)
aDoc.writeDocument("## %s\n\n" % chTitle)
@@ -342,7 +329,7 @@ class NWProject():
if numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
sHandle = self.newFile(scTitle, pHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -351,7 +338,7 @@ class NWProject():
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
sHandle = self.newFile(scTitle, nHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -481,8 +468,9 @@ class NWProject():
# documents and one for project notes. Introduced in
# version 1.5.
# 1.4 : Introduces a more compact format for storing items. All
# settings aside from name are now attributes. Introduced
# in version 1.7.
# settings aside from name are now attributes. This format
# also changes the way satus and importance labels are
# stored and handled. Introduced in version 1.7.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
self.theParent.makeAlert(self.tr(
@@ -613,7 +601,13 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
# Check the project tree consistency
for tItem in self.projTree:
tHandle = tItem.itemHandle
logger.verbose("Checking item '%s'", tHandle)
if not self.projTree.updateItemData(tHandle):
logger.error("There was a problem item '%s', and it has been removed", tHandle)
del self.projTree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
self._loadProjectLocalisation()
@@ -624,6 +618,7 @@ class NWProject():
self._writeLockFile()
self.setProjectChanged(False)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True
@@ -1202,7 +1197,7 @@ class NWProject():
self.statusItems.resetCounts()
self.importItems.resetCounts()
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
if nwItem.isNovelLike():
self.statusItems.increment(nwItem.itemStatus)
else:
self.importItems.increment(nwItem.itemImport)
@@ -1452,6 +1447,7 @@ class NWProject():
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem)
self.projTree.updateItemData(orphItem.itemHandle)
if noWhere:
self.theParent.makeAlert(self.tr(
+78 -72
View File
@@ -33,7 +33,7 @@ from hashlib import sha256
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
from novelwriter.constants import nwConst, nwFiles
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -41,13 +41,15 @@ logger = logging.getLogger(__name__)
class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
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._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
@@ -67,7 +69,7 @@ class NWTree():
"""
self._projTree = {}
self._treeOrder = []
self._treeRoots = []
self._treeRoots = {}
self._trashRoot = None
self._archRoot = None
self._theIndex = 0
@@ -98,7 +100,7 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle)
self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
@@ -207,9 +209,30 @@ class NWTree():
return novelWords, noteWords
##
# Tree Structure Methods
# Tree Item Methods
##
def updateItemData(self, tHandle):
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
iItem = tItem
for _ in range(self.MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
return True
else:
iItem = self.__getitem__(iItem.itemParent)
if iItem is None:
return False
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type.
"""
@@ -218,71 +241,6 @@ class NWTree():
return False
return tItem.itemType == itemType
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 isTrashRoot(self, tHandle):
"""Check if a handle is the trash folder.
"""
if self._trashRoot is None:
return False
return tHandle == self._trashRoot
def archiveRoot(self):
"""Returns the handle of the archive folder, or None if there
isn't one.
"""
if self._archRoot:
return self._archRoot
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 tItem is None:
continue
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 to make infinite loops impossible.
"""
tItem = self.__getitem__(tHandle)
if tItem is not None:
for i in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tItem
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(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.
@@ -293,7 +251,7 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1):
for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None:
return tTree
else:
@@ -303,8 +261,56 @@ class NWTree():
return tTree
else:
tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
return tTree
##
# Tree Root Methods
##
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
return tHandle in self._treeRoots
def isTrash(self, tHandle):
"""Check if an item is in or is the trash folder.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
if tItem.itemClass == nwItemClass.TRASH:
return True
if self._trashRoot is not None:
if tHandle == self._trashRoot:
return True
elif tItem.itemParent == self._trashRoot:
return True
elif tItem.itemRoot == self._trashRoot:
return True
return False
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 first root item for a given class.
"""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return tItem.itemHandle
return None
##
# Setters
##
@@ -420,7 +426,7 @@ class NWTree():
return
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
del self._treeRoots[tHandle]
if tHandle == self._trashRoot:
self._trashRoot = None
if tHandle == self._archRoot:
+1 -1
View File
@@ -130,7 +130,7 @@ class GuiDocMerge(QDialog):
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
+3 -20
View File
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout
from novelwriter.constants import nwConst
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__)
@@ -160,16 +159,6 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR)
return False
# Check that another folder can be created
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.MAX_DEPTH - 1:
self.theParent.makeAlert(self.tr(
"Cannot add new folder for the document split. "
"Maximum folder depth has been reached. "
"Please move the file to another level in the project tree."
), nwAlert.ERROR)
return False
msgYes = self.theParent.askQuestion(
self.tr("Split Document"),
"{0}<br><br>{1}".format(
@@ -186,22 +175,16 @@ class GuiDocSplit(QDialog):
return False
# Create the folder
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#").strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.projTree[nHandle]
newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
logger.verbose(
+3 -3
View File
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.constants import trConst, nwLists, nwLabels
from novelwriter.constants import trConst, nwLabels
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -74,7 +74,7 @@ class GuiItemEditor(QDialog):
# Item Status
self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL:
if self.theItem.isNovelLike():
for key, entry in self.theProject.statusItems.items():
self.editStatus.addItem(entry["icon"], entry["name"], key)
@@ -95,7 +95,7 @@ class GuiItemEditor(QDialog):
self.editLayout.setMinimumWidth(mVd)
validLayouts = []
if self.theItem.itemType == nwItemType.FILE:
if self.theItem.itemClass in nwLists.CLS_NOVEL:
if self.theItem.documentAllowed():
validLayouts.append(nwItemLayout.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE)
else:
+2 -20
View File
@@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar):
return
##
# Methods
##
def setAvailableRoot(self):
"""Update the list of available root folders and set the ones
that are active.
"""
for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS:
continue
if itemClass == nwItemClass.TRASH:
continue
self.rootItems[itemClass].setVisible(
self.theProject.projTree.checkRootUnique(itemClass)
)
return
##
# Update Menu on Settings Changed
##
@@ -215,7 +197,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder
self.aCreateFolder = QAction(self.tr("Create Folder"), self)
self.aCreateFolder.setShortcut("Ctrl+Shift+N")
self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER, None))
self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER))
self.projMenu.addAction(self.aCreateFolder)
# Project > Separator
@@ -277,7 +259,7 @@ class GuiMainMenu(QMenuBar):
# Document > New
self.aNewDoc = QAction(self.tr("New Document"), self)
self.aNewDoc.setShortcut("Ctrl+N")
self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE, None))
self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE))
self.docuMenu.addAction(self.aNewDoc)
# Document > Open
+88 -163
View File
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.constants import nwConst, trConst, nwLists, nwLabels
from novelwriter.constants import trConst, nwLabels
logger = logging.getLogger(__name__)
@@ -161,114 +161,64 @@ class GuiProjectTree(QTreeWidget):
self._timeChanged = 0
return
def newTreeItem(self, itemType, itemClass):
"""Add new item to the tree, with a given itemType and
itemClass, and attach it to the selected handle. Also make sure
the item is added in a place it can be added, and that other
def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also make
sure the item is added in a place it can be added, and that other
meta data is set correctly to ensure a valid project tree.
"""
pHandle = self.getSelectedHandle()
nHandle = None
if not self.theParent.hasProject:
logger.error("No project open")
return False
if not isinstance(itemType, nwItemType):
# This would indicate an internal bug
logger.error("No itemType provided")
return False
nHandle = None
tHandle = None
# The item needs to be assigned an item class, so one must be
# provided, or it must be possible to extract it from the parent
# item of the new item.
if itemClass is None and pHandle is not None:
pItem = self.theProject.projTree[pHandle]
if pItem is not None:
itemClass = pItem.itemClass
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# If class is still not set, alert the user and exit
if itemClass is None:
if itemType == nwItemType.FILE:
self.theParent.makeAlert(self.tr(
"Please select a valid location in the tree to add the document."
), nwAlert.ERROR)
else:
self.theParent.makeAlert(self.tr(
"Please select a valid location in the tree to add the folder."
), nwAlert.ERROR)
return False
# Everything is fine, we have what we need, so we proceed
logger.verbose(
"Adding new item of type '%s' and class '%s' to handle '%s'",
itemType.name, itemClass.name, str(pHandle)
)
if itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot(
trConst(nwLabels.CLASS_NAME[itemClass]), itemClass
)
if tHandle is None:
logger.error("No root item added")
return False
else:
# If no parent has been selected, make the new file under
# the root NOVEL item.
if pHandle is None:
pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL)
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
# If still nothing, give up
if pHandle is None:
sHandle = self.getSelectedHandle()
if sHandle is None or sHandle not in self.theProject.projTree:
self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
# Now check if the selected item is a file, in which case
# the new file will be a sibling
pItem = self.theProject.projTree[pHandle]
# If the selected item is a file, the new item will be a sibling
pItem = self.theProject.projTree[sHandle]
if pItem.itemType == nwItemType.FILE:
nHandle = pHandle
pHandle = pItem.itemParent
nHandle = sHandle
sHandle = pItem.itemParent
if sHandle is None:
logger.error("Internal error") # Bug
return False
# If we again have no home, give up
if pHandle is None:
self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
if self.theProject.projTree.isTrashRoot(pHandle):
if self.theProject.projTree.isTrash(sHandle):
self.theParent.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR)
return False
parTree = self.theProject.projTree.getItemPath(pHandle)
# If we're still here, add the file or folder
# Add the file or folder
if itemType == nwItemType.FILE:
tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle)
if pItem.isNovelLike():
tHandle = self.theProject.newFile(self.tr("New Document"), sHandle)
else:
tHandle = self.theProject.newFile(self.tr("New Note"), sHandle)
elif itemType == nwItemType.FOLDER:
if len(parTree) >= nwConst.MAX_DEPTH - 1:
# Folders cannot be deeper than MAX_DEPTH - 1, leaving room
# for one more level of files.
self.theParent.makeAlert(self.tr(
"Cannot add new folder to this item. "
"Maximum folder depth has been reached."
), nwAlert.ERROR)
return False
tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle)
tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle)
else:
logger.error("Failed to add new item")
return False
else:
logger.error("Failed to add new item")
return False
# If there is no handle set, return here
if tHandle is None:
# If there is no handle set, return here. This is a bug
if tHandle is None: # pragma: no cover
return True
# Add the new item to the tree
@@ -280,13 +230,9 @@ class GuiProjectTree(QTreeWidget):
if nwItem.itemType != nwItemType.FILE:
return True
# This is a new files, so let's add some content
# This is a new file, so let's add some content
newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument()
if curTxt is None:
curTxt = ""
if curTxt == "":
if not newDoc.readDocument():
if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n"
else:
@@ -310,6 +256,9 @@ class GuiProjectTree(QTreeWidget):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
return False
trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None:
return False
@@ -495,8 +444,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item")
return False
pHandle = nwItemS.itemParent
if self.theProject.projTree.isTrashRoot(pHandle):
if self.theProject.projTree.isTrash(tHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
@@ -513,13 +461,6 @@ class GuiProjectTree(QTreeWidget):
if doPermanent:
logger.debug("Permanently deleting file with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.docHandle() == tHandle:
self.theParent.closeDocument()
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.theParent.makeAlert([
@@ -527,6 +468,13 @@ class GuiProjectTree(QTreeWidget):
], nwAlert.ERROR)
return False
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.docHandle() == tHandle:
self.theParent.closeDocument()
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
@@ -540,19 +488,13 @@ class GuiProjectTree(QTreeWidget):
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
if pHandle is None:
logger.warning("File has no parent item")
logger.debug("Moving file '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._updateItemParent(tHandle)
self.propagateCount(tHandle, wCount)
self.theIndex.deleteHandle(tHandle)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True)
@@ -562,6 +504,7 @@ class GuiProjectTree(QTreeWidget):
if trItemP is None:
logger.error("Could not delete folder")
return False
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
@@ -581,7 +524,6 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
else:
self.theParent.makeAlert(self.tr(
@@ -634,7 +576,7 @@ class GuiProjectTree(QTreeWidget):
return
def propagateCount(self, tHandle, theCount, nDepth=0):
def propagateCount(self, tHandle, theCount):
"""Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating
@@ -654,12 +596,13 @@ class GuiProjectTree(QTreeWidget):
return
pCount = 0
pHandle = None
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
if pHandle:
self.propagateCount(pHandle, pCount)
return
@@ -714,9 +657,7 @@ class GuiProjectTree(QTreeWidget):
movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem)
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._postItemMove(sHandle, wCount)
self.clearSelection()
movItem.setSelected(True)
@@ -848,29 +789,34 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None:
pIndex = pItem.indexOfChild(sItem)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
# Determine if the drag and drop is allowed:
# - Files can be moved anywhere
# - Folders can only be moved within the same root folder
# - Root folders cannot be moved at all
# - Items cannot be dropped on top of a file (moved inside)
isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE
inSame = snItem.itemRoot == dnItem.itemRoot
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
allowDrop = isSame or isNone or isNote or onFree
allowDrop = inSame or isFile
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
else:
theEvent.ignore()
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
theEvent.ignore()
self.theParent.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
@@ -881,40 +827,39 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions
##
def _postItemMove(self, sHandle, snItem, dnItem, wCount):
def _postItemMove(self, tHandle, wCount):
"""Run various maintenance tasks for a moved item.
"""
isFile = snItem.itemType == nwItemType.FILE
isSame = snItem.itemClass == dnItem.itemClass
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Failed to find new parent item of '%s'", tHandle)
return False
self._updateItemParent(sHandle)
# Update item parent handle in the project, make sure meta data
# is updated accordingly, and update word count
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.theProject.projTree.updateItemData(tHandle)
self.setTreeItemValues(tHandle)
self.propagateCount(tHandle, wCount)
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not (isSame or onFree):
logger.debug(
"Item '%s' class has been changed from '%s' to '%s'",
sHandle, snItem.itemClass.name, dnItem.itemClass.name
)
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
# The items dropped into archive or trash should be removed
# from the project index, for all other items, we rescan the
# file to ensure the index is up to date.
if onFree:
self.theIndex.deleteHandle(sHandle)
if nwItemS.isInactive():
self.theIndex.deleteHandle(tHandle)
else:
self.theIndex.reIndexHandle(sHandle)
self.theIndex.reIndexHandle(tHandle)
# Trigger dependent updates
self._setTreeChanged(True)
self._emitItemChange(sHandle)
self._emitItemChange(tHandle)
return
return True
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
@@ -966,7 +911,6 @@ class GuiProjectTree(QTreeWidget):
if pHandle is None:
if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem)
self.theParent.mainMenu.setAvailableRoot()
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem)
else:
@@ -1015,25 +959,6 @@ class GuiProjectTree(QTreeWidget):
return trItem
def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information
in the project is consistent with the treeView.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Failed to find new parent item of '%s'", tHandle)
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
return True
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
@@ -1048,7 +973,7 @@ class GuiProjectTree(QTreeWidget):
"""
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
nwItem = self.theProject.projTree[tHandle]
if nwItem.itemClass == nwItemClass.NOVEL:
if nwItem.isNovelLike():
self.novelItemChanged.emit()
else:
self.noteItemChanged.emit()
@@ -1182,7 +1107,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new file call to the project tree.
"""
if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FILE, None)
self.theTree.newTreeItem(nwItemType.FILE)
return
@pyqtSlot()
@@ -1190,7 +1115,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new folder call to the project tree.
"""
if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FOLDER, None)
self.theTree.newTreeItem(nwItemType.FOLDER)
return
@pyqtSlot()
+1 -2
View File
@@ -55,7 +55,6 @@ from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwLists
logger = logging.getLogger(__name__)
@@ -848,7 +847,7 @@ class GuiMain(QMainWindow):
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return False
if tItem.itemType not in nwLists.REG_TYPES:
if tItem.itemType == nwItemType.NO_TYPE:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
+2 -8
View File
@@ -780,14 +780,12 @@ class GuiBuildNovel(QDialog):
if theItem is None:
return False
if not theItem.isExported and not ignoreFlag:
if not (theItem.isExported or ignoreFlag):
return False
isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.isInactive()
isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
@@ -799,10 +797,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles:
return False
rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle)
if rootItem.itemClass == nwItemClass.ARCHIVE:
return False
return True
def _saveDocument(self, theFmt):
+30 -30
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-05 23:14:56">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-17 19:05:24">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1303</saveCount>
<saveCount>1306</saveCount>
<autoCount>199</autoCount>
<editTime>65049</editTime>
<editTime>65149</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -33,7 +33,7 @@
<section></section>
</titleFormat>
<status>
<entry key="sf12341" count="6" red="100" green="100" blue="100">New</entry>
<entry key="sf12341" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry>
<entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry>
<entry key="s90e6c9" count="6" red="193" green="129" blue="0">1st Draft</entry>
@@ -42,110 +42,110 @@
<entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
</status>
<importance>
<entry key="ia857f0" count="4" red="100" green="100" blue="100">None</entry>
<entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry>
<entry key="icfb3a5" count="2" red="0" green="122" blue="188">Minor</entry>
<entry key="i2d7a54" count="2" red="21" green="0" blue="180">Major</entry>
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance>
</settings>
<content count="25">
<item handle="7031beac91f75" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="93" wordCount="19" paraCount="2" cursorPos="2"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="186" wordCount="39" paraCount="2" cursorPos="212"/>
<name status="sf12341" import="ia857f0" exported="True">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="26" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="sf12341" import="ia857f0" exported="True">Part One</name>
</item>
<item handle="e7ded148d6e4a" parent="7031beac91f75" order="3" type="FOLDER" class="NOVEL">
<item handle="e7ded148d6e4a" parent="7031beac91f75" root="7031beac91f75" order="3" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s90e6c9" import="ia857f0">A Folder</name>
</item>
<item handle="6a2d6d5f4f401" parent="e7ded148d6e4a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="6a2d6d5f4f401" parent="e7ded148d6e4a" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="75" wordCount="14" paraCount="1" cursorPos="279"/>
<name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="e7ded148d6e4a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="636b6aa9b697b" parent="e7ded148d6e4a" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2429" wordCount="432" paraCount="14" cursorPos="219"/>
<name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="e7ded148d6e4a" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="bc0cbd2a407f3" parent="e7ded148d6e4a" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="476" wordCount="93" paraCount="3" cursorPos="577"/>
<name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="e7ded148d6e4a" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="ba8a28a246524" parent="e7ded148d6e4a" root="7031beac91f75" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="617" wordCount="101" paraCount="3" cursorPos="4"/>
<name status="sf12341" import="ia857f0" exported="True">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="e7ded148d6e4a" order="4" type="FILE" class="NOVEL" layout="NOTE">
<item handle="96b68994dfa3d" parent="e7ded148d6e4a" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="NOTE">
<meta charCount="1692" wordCount="313" paraCount="6" cursorPos="1110"/>
<name status="sd51c5b" import="ia857f0" exported="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="e7ded148d6e4a" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="88706ddc78b1b" parent="e7ded148d6e4a" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="e7ded148d6e4a" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="ae7339df26ded" parent="e7ded148d6e4a" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="189" wordCount="37" paraCount="1" cursorPos="224"/>
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
</item>
<item handle="f6622b4617424" parent="None" order="1" type="ROOT" class="CHARACTER">
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Characters</name>
</item>
<item handle="f7e2d9f330615" parent="f6622b4617424" order="0" type="FOLDER" class="CHARACTER">
<item handle="f7e2d9f330615" parent="f6622b4617424" root="f6622b4617424" order="0" type="FOLDER" class="CHARACTER">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Main Characters</name>
</item>
<item handle="14298de4d9524" parent="f7e2d9f330615" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="sf12341" import="icfb3a5" exported="True">John Smith</name>
</item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
</item>
<item handle="15c4492bd5107" parent="None" order="2" type="ROOT" class="WORLD">
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="2" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Locations</name>
</item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="sf12341" import="i56be10" exported="True">Earth</name>
</item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="sf12341" import="icfb3a5" exported="True">Space</name>
</item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" exported="True">Mars</name>
</item>
<item handle="6827118336ac1" parent="None" order="3" type="ROOT" class="ARCHIVE">
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="3" type="ROOT" class="ARCHIVE">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Archive</name>
</item>
<item handle="ae9bf3c3ea159" parent="6827118336ac1" order="0" type="FOLDER" class="ARCHIVE">
<item handle="ae9bf3c3ea159" parent="6827118336ac1" root="6827118336ac1" order="0" type="FOLDER" class="ARCHIVE">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Scenes</name>
</item>
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" root="6827118336ac1" order="0" type="FILE" class="ARCHIVE" layout="DOCUMENT">
<meta charCount="315" wordCount="55" paraCount="1" cursorPos="322"/>
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
</item>
<item handle="98acd8c76c93a" parent="None" order="4" type="TRASH" class="TRASH">
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="4" type="TRASH" class="TRASH">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Trash</name>
</item>
<item handle="b8136a5a774a0" parent="98acd8c76c93a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="b8136a5a774a0" parent="98acd8c76c93a" root="98acd8c76c93a" order="0" type="FILE" class="TRASH" layout="DOCUMENT">
<meta charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Delete Me!</name>
</item>
+3 -1
View File
@@ -166,7 +166,9 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
"""
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr("novelwriter.CONFIG", fncConf)
nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir])
nwGUI = novelwriter.main(
["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir]
)
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.wait(20)
+21 -21
View File
@@ -44,87 +44,87 @@
</importance>
</settings>
<content count="21">
<item handle="b3643d0f92e32" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Novel</name>
</item>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
</item>
<item handle="8c58a65414c23" parent="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" exported="True">Front Matter</name>
</item>
<item handle="88d59a277361b" parent="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" exported="True">Prologue</name>
</item>
<item handle="db7e733775d4d" parent="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" exported="True">Act One</name>
</item>
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" root="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter One</name>
</item>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter One</name>
</item>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" exported="True">Scene One</name>
</item>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" exported="True">Scene Two</name>
</item>
<item handle="846352075de7d" parent="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" exported="False">Interlude</name>
</item>
<item handle="6bd935d2490cd" parent="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
<item handle="6bd935d2490cd" parent="b3643d0f92e32" root="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter Two</name>
</item>
<item handle="441420a886d82" parent="6bd935d2490cd" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
</item>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" exported="True">Scene Three</name>
</item>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" exported="True">Scene Four</name>
</item>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" exported="True">Scene Five</name>
</item>
<item handle="67a8707f2f249" parent="None" order="1" type="ROOT" class="CHARACTER">
<item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Characters</name>
</item>
<item handle="4c4f28287af27" parent="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
</item>
<item handle="6c6afb1247750" parent="None" order="2" type="ROOT" class="PLOT">
<item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Plot</name>
</item>
<item handle="2426c6f0ca922" parent="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" exported="True">Main</name>
</item>
<item handle="60bdf227455cc" parent="None" order="3" type="ROOT" class="WORLD">
<item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">World</name>
</item>
<item handle="04468803b92e1" parent="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
</item>
+8 -8
View File
@@ -42,35 +42,35 @@
</importance>
</settings>
<content count="8">
<item handle="a508bb932959c" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="a508bb932959c" parent="None" root="a508bb932959c" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="s72322d" import="iffcacb">Novel</name>
</item>
<item handle="a35baf2e93843" parent="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" exported="True">Title Page</name>
</item>
<item handle="a6d311a93600a" parent="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
<item handle="a6d311a93600a" parent="a508bb932959c" root="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s72322d" import="iffcacb">New Chapter</name>
</item>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
</item>
<item handle="8c659a11cd429" parent="a6d311a93600a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" exported="True">New Scene</name>
</item>
<item handle="7695ce551d265" parent="None" order="1" type="ROOT" class="PLOT">
<item handle="7695ce551d265" parent="None" root="7695ce551d265" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">Plot</name>
</item>
<item handle="afb3043c7b2b3" parent="None" order="2" type="ROOT" class="CHARACTER">
<item handle="afb3043c7b2b3" parent="None" root="afb3043c7b2b3" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">Characters</name>
</item>
<item handle="9d5247ab588e0" parent="None" order="3" type="ROOT" class="WORLD">
<item handle="9d5247ab588e0" parent="None" root="9d5247ab588e0" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">World</name>
</item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 14:14:40">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:40:13">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -35,104 +35,104 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="ie465e1" count="0" red="100" green="100" blue="100">New</entry>
<entry key="ie465e1" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i8b9d24" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="23">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Novel</name>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="None">Plot</name>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="None">Characters</name>
<name status="sbc8960" import="ie465e1">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="None">Locations</name>
<name status="sbc8960" import="ie465e1">Locations</name>
</item>
<item handle="25fc0e7096fc6" parent="None" order="0" type="ROOT" class="TIMELINE">
<item handle="25fc0e7096fc6" parent="None" root="25fc0e7096fc6" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name>
<name status="sbc8960" import="ie465e1">Timeline</name>
</item>
<item handle="31489056e0916" parent="None" order="0" type="ROOT" class="OBJECT">
<item handle="31489056e0916" parent="None" root="31489056e0916" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="sbc8960" import="None">Objects</name>
<name status="sbc8960" import="ie465e1">Objects</name>
</item>
<item handle="98010bd9270f9" parent="None" order="0" type="ROOT" class="ENTITY">
<item handle="98010bd9270f9" parent="None" root="98010bd9270f9" order="0" type="ROOT" class="ENTITY">
<meta expanded="False"/>
<name status="sbc8960" import="None">Entities</name>
<name status="sbc8960" import="ie465e1">Entities</name>
</item>
<item handle="0e17daca5f3e1" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Title Page</name>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item>
<item handle="1a6562590ef19" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="1a6562590ef19" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 1</name>
<name status="sbc8960" import="ie465e1">Chapter 1</name>
</item>
<item handle="031b4af5197ec" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="031b4af5197ec" parent="1a6562590ef19" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Chapter 1</name>
<name status="sbc8960" import="ie465e1" exported="True">Chapter 1</name>
</item>
<item handle="41cfc0d1f2d12" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="41cfc0d1f2d12" parent="1a6562590ef19" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 1.1</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 1.1</name>
</item>
<item handle="2858dcd1057d3" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="2858dcd1057d3" parent="1a6562590ef19" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 1.2</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 1.2</name>
</item>
<item handle="2fca346db6561" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="2fca346db6561" parent="1a6562590ef19" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 1.3</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 1.3</name>
</item>
<item handle="02d20bbd7e394" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="02d20bbd7e394" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 2</name>
<name status="sbc8960" import="ie465e1">Chapter 2</name>
</item>
<item handle="7688b6ef52555" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="7688b6ef52555" parent="02d20bbd7e394" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Chapter 2</name>
<name status="sbc8960" import="ie465e1" exported="True">Chapter 2</name>
</item>
<item handle="c837649cce43f" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="c837649cce43f" parent="02d20bbd7e394" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 2.1</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 2.1</name>
</item>
<item handle="6208ef0f7750c" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="6208ef0f7750c" parent="02d20bbd7e394" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 2.2</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 2.2</name>
</item>
<item handle="3e1e967e9b793" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="3e1e967e9b793" parent="02d20bbd7e394" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 2.3</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 2.3</name>
</item>
<item handle="39fa9ec190eee" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="39fa9ec190eee" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 3</name>
<name status="sbc8960" import="ie465e1">Chapter 3</name>
</item>
<item handle="d029fa3a95e17" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="d029fa3a95e17" parent="39fa9ec190eee" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Chapter 3</name>
<name status="sbc8960" import="ie465e1" exported="True">Chapter 3</name>
</item>
<item handle="81b8a03f97e87" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="81b8a03f97e87" parent="39fa9ec190eee" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 3.1</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 3.1</name>
</item>
<item handle="da4ea2a5506f2" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="da4ea2a5506f2" parent="39fa9ec190eee" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 3.2</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 3.2</name>
</item>
<item handle="a68b412c42825" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="a68b412c42825" parent="39fa9ec190eee" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 3.3</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 3.3</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 14:15:44">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:39:10">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -35,68 +35,68 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="ie465e1" count="0" red="100" green="100" blue="100">New</entry>
<entry key="ie465e1" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i8b9d24" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="14">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Novel</name>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="None">Plot</name>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="None">Characters</name>
<name status="sbc8960" import="ie465e1">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="None">Locations</name>
<name status="sbc8960" import="ie465e1">Locations</name>
</item>
<item handle="25fc0e7096fc6" parent="None" order="0" type="ROOT" class="TIMELINE">
<item handle="25fc0e7096fc6" parent="None" root="25fc0e7096fc6" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name>
<name status="sbc8960" import="ie465e1">Timeline</name>
</item>
<item handle="31489056e0916" parent="None" order="0" type="ROOT" class="OBJECT">
<item handle="31489056e0916" parent="None" root="31489056e0916" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="sbc8960" import="None">Objects</name>
<name status="sbc8960" import="ie465e1">Objects</name>
</item>
<item handle="98010bd9270f9" parent="None" order="0" type="ROOT" class="ENTITY">
<item handle="98010bd9270f9" parent="None" root="98010bd9270f9" order="0" type="ROOT" class="ENTITY">
<meta expanded="False"/>
<name status="sbc8960" import="None">Entities</name>
<name status="sbc8960" import="ie465e1">Entities</name>
</item>
<item handle="0e17daca5f3e1" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Title Page</name>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item>
<item handle="1a6562590ef19" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="1a6562590ef19" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 1</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 1</name>
</item>
<item handle="031b4af5197ec" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="031b4af5197ec" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 2</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 2</name>
</item>
<item handle="41cfc0d1f2d12" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="41cfc0d1f2d12" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 3</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 3</name>
</item>
<item handle="2858dcd1057d3" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="2858dcd1057d3" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 4</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 4</name>
</item>
<item handle="2fca346db6561" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="2fca346db6561" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 5</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 5</name>
</item>
<item handle="02d20bbd7e394" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="02d20bbd7e394" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Scene 6</name>
<name status="sbc8960" import="ie465e1" exported="True">Scene 6</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 14:17:50">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:37:42">
<project>
<name>New Project</name>
<title></title>
@@ -33,52 +33,52 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="ie465e1" count="3" red="100" green="100" blue="100">New</entry>
<entry key="ie465e1" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i8b9d24" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="10">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item>
<item handle="1a6562590ef19" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="1a6562590ef19" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Hello</name>
<name status="sbc8960" import="ie465e1" exported="True">Hello</name>
</item>
<item handle="031b4af5197ec" parent="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<item handle="031b4af5197ec" parent="71ee45a3c0db9" root="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="None" exported="True">Jane</name>
<name status="sbc8960" import="ie465e1" exported="True">Jane</name>
</item>
</content>
</novelWriterXML>
@@ -40,35 +40,35 @@
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 14:16:49">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:38:35">
<project>
<name>New Project</name>
<title></title>
@@ -27,66 +27,82 @@
<section></section>
</titleFormat>
<status>
<entry key="sbc8960" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sbc8960" count="6" red="100" green="100" blue="100">New</entry>
<entry key="s1a3d1f" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="sad3c2d" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="ie465e1" count="3" red="100" green="100" blue="100">New</entry>
<entry key="ie465e1" count="10" red="100" green="100" blue="100">New</entry>
<entry key="i8b9d24" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<content count="16">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item>
<item handle="1a6562590ef19" parent="None" order="0" type="ROOT" class="TIMELINE">
<item handle="1a6562590ef19" parent="None" root="1a6562590ef19" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name>
<name status="sbc8960" import="ie465e1">Novel</name>
</item>
<item handle="031b4af5197ec" parent="None" order="0" type="ROOT" class="OBJECT">
<item handle="031b4af5197ec" parent="None" root="031b4af5197ec" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sbc8960" import="None">Object</name>
<name status="sbc8960" import="ie465e1">Plot</name>
</item>
<item handle="41cfc0d1f2d12" parent="None" order="0" type="ROOT" class="CUSTOM">
<item handle="41cfc0d1f2d12" parent="None" root="41cfc0d1f2d12" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sbc8960" import="None">Custom1</name>
<name status="sbc8960" import="ie465e1">Character</name>
</item>
<item handle="2858dcd1057d3" parent="None" order="0" type="ROOT" class="CUSTOM">
<item handle="2858dcd1057d3" parent="None" root="2858dcd1057d3" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sbc8960" import="None">Custom2</name>
<name status="sbc8960" import="ie465e1">World</name>
</item>
<item handle="2fca346db6561" parent="None" root="2fca346db6561" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Timeline</name>
</item>
<item handle="02d20bbd7e394" parent="None" root="02d20bbd7e394" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Object</name>
</item>
<item handle="7688b6ef52555" parent="None" root="7688b6ef52555" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Custom1</name>
</item>
<item handle="c837649cce43f" parent="None" root="c837649cce43f" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Custom2</name>
</item>
</content>
</novelWriterXML>
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 44cb730c42048/031b4af5197ec
%%~kind: PLOT/NOTE
# Main Plot
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 71ee45a3c0db9/1a6562590ef19
%%~kind: CHARACTER/NOTE
# Jane Doe
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 811786ad1ae74/41cfc0d1f2d12
%%~kind: WORLD/NOTE
# Main Location
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 12:58:48">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-17 14:18:28">
<project>
<name>New Project</name>
<title></title>
@@ -33,60 +33,60 @@
<entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i466852" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i466852" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i3eb13b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sa3b179" import="i466852">Novel</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="i466852" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="sa3b179" import="i466852">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="i466852" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="612" wordCount="95" paraCount="10" cursorPos="768"/>
<name status="sa3b179" import="i466852" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" parent="None" order="1" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="1" type="ROOT" class="PLOT">
<meta expanded="True"/>
<name status="sa3b179" import="i466852">Plot</name>
</item>
<item handle="031b4af5197ec" parent="44cb730c42048" order="0" type="FILE" class="PLOT" layout="NOTE">
<item handle="031b4af5197ec" parent="44cb730c42048" root="44cb730c42048" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta charCount="48" wordCount="10" paraCount="1" cursorPos="69"/>
<name status="sa3b179" import="None" exported="True">New File</name>
<name status="sa3b179" import="i466852" exported="True">New Note</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="2" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="sa3b179" import="i466852">Characters</name>
</item>
<item handle="1a6562590ef19" parent="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<item handle="1a6562590ef19" parent="71ee45a3c0db9" root="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="34" wordCount="8" paraCount="1" cursorPos="51"/>
<name status="sa3b179" import="None" exported="True">New File</name>
<name status="sa3b179" import="i466852" exported="True">New Note</name>
</item>
<item handle="811786ad1ae74" parent="None" order="3" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sa3b179" import="i466852">World</name>
</item>
<item handle="41cfc0d1f2d12" parent="811786ad1ae74" order="0" type="FILE" class="WORLD" layout="NOTE">
<item handle="41cfc0d1f2d12" parent="811786ad1ae74" root="811786ad1ae74" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="sa3b179" import="None" exported="True">New File</name>
<name status="sa3b179" import="i466852" exported="True">New Note</name>
</item>
<item handle="2fca346db6561" parent="None" order="4" type="TRASH" class="TRASH">
<item handle="2fca346db6561" parent="None" root="2fca346db6561" order="4" type="TRASH" class="TRASH">
<meta expanded="True"/>
<name status="None" import="None">Trash</name>
<name status="sa3b179" import="i466852">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -33,44 +33,44 @@
<entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i466852" count="0" red="100" green="100" blue="100">New</entry>
<entry key="i466852" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i3eb13b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sa3b179" import="None">Novel</name>
<name status="sa3b179" import="i466852">Novel</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">Title Page</name>
<name status="sa3b179" import="i466852" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sa3b179" import="None">New Chapter</name>
<name status="sa3b179" import="i466852">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">New Chapter</name>
<name status="sa3b179" import="i466852" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">New Scene</name>
<name status="sa3b179" import="i466852" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" parent="None" order="1" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sa3b179" import="None">Plot</name>
<name status="sa3b179" import="i466852">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="2" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sa3b179" import="None">Characters</name>
<name status="sa3b179" import="i466852">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="3" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sa3b179" import="None">World</name>
<name status="sa3b179" import="i466852">World</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 18:15:12">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:52:19">
<project>
<name>Project Name</name>
<title>Project Title</title>
@@ -39,44 +39,44 @@
<entry key="sbc8960" count="0" red="20" green="30" blue="40">Final</entry>
</status>
<importance>
<entry key="i466852" count="0" red="100" green="100" blue="100">New</entry>
<entry key="i466852" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i3eb13b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i1a3d1f" count="0" red="100" green="100" blue="100">Final</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<item handle="73475cb40a568" parent="None" root="73475cb40a568" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="sa3b179" import="None">Novel</name>
<name status="sa3b179" import="i466852">Novel</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="25fc0e7096fc6" parent="73475cb40a568" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">Title Page</name>
<name status="sa3b179" import="i466852" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<item handle="31489056e0916" parent="73475cb40a568" root="73475cb40a568" order="1" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="sa3b179" import="None">New Chapter</name>
<name status="sa3b179" import="i466852">New Chapter</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="98010bd9270f9" parent="31489056e0916" root="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">New Chapter</name>
<name status="sa3b179" import="i466852" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<item handle="0e17daca5f3e1" parent="31489056e0916" root="73475cb40a568" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="None" exported="True">New Scene</name>
<name status="sa3b179" import="i466852" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" parent="None" order="1" type="ROOT" class="PLOT">
<item handle="44cb730c42048" parent="None" root="44cb730c42048" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="sa3b179" import="None">Plot</name>
<name status="sa3b179" import="i466852">Plot</name>
</item>
<item handle="71ee45a3c0db9" parent="None" order="2" type="ROOT" class="CHARACTER">
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="sa3b179" import="None">Characters</name>
<name status="sa3b179" import="i466852">Characters</name>
</item>
<item handle="811786ad1ae74" parent="None" order="3" type="ROOT" class="WORLD">
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="sa3b179" import="None">World</name>
<name status="sa3b179" import="i466852">World</name>
</item>
</content>
</novelWriterXML>
+1 -1
View File
@@ -66,7 +66,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
# Try to open a new (non-existent) file
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
assert nHandle is not None
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
xHandle = theProject.newFile("New File", nHandle)
theDoc = NWDoc(theProject, xHandle)
assert bool(theDoc) is True
assert repr(theDoc) == f"<NWDoc handle={xHandle}>"
+18 -14
View File
@@ -181,8 +181,8 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI):
assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle]
@@ -260,8 +260,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
theIndex = NWIndex(theProject)
# Some items for fail to scan tests
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c")
dHandle = theProject.newFolder("Folder", "a508bb932959c")
xHandle = theProject.newFile("No Layout", "a508bb932959c")
xItem = theProject.projTree[xHandle]
xItem.setLayout(nwItemLayout.NO_LAYOUT)
@@ -278,20 +278,24 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle)
theProject.projTree.updateItemData(xItem.itemHandle)
assert xItem.itemRoot == tHandle
assert xItem.itemClass == nwItemClass.TRASH
assert theIndex.scanText(xHandle, "Hello World!") is False
# Create the archive root
aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE)
assert theProject.projTree[aHandle] is not None
xItem.setParent(aHandle)
theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items
tHandle = theProject.newFile("Title", nwItemClass.NOVEL, "a508bb932959c")
pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c")
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c")
tHandle = theProject.newFile("Title", "a508bb932959c")
pHandle = theProject.newFile("Page", "a508bb932959c")
nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
sHandle = theProject.newFile("Scene", "a508bb932959c")
# Text Indexing
# =============
@@ -473,8 +477,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
assert theIndex.getNovelData("", "") is None
assert theIndex.getNovelData("a508bb932959c", "") is None
@@ -628,9 +632,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# Novel Stats
# ===========
hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c")
sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c")
tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c")
hHandle = theProject.newFile("Chapter", "a508bb932959c")
sHandle = theProject.newFile("Scene One", "a508bb932959c")
tHandle = theProject.newFile("Scene Two", "a508bb932959c")
theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT
theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
+136 -7
View File
@@ -71,6 +71,18 @@ def testCoreItem_Setters(mockGUI, constData):
theItem.setParent("0123456789abc")
assert theItem.itemParent == "0123456789abc"
# Root
theItem.setRoot(None)
assert theItem.itemRoot is None
theItem.setRoot(123)
assert theItem.itemRoot is None
theItem.setRoot("0123456789abcdef")
assert theItem.itemRoot is None
theItem.setRoot("0123456789abg")
assert theItem.itemRoot is None
theItem.setRoot("0123456789abc")
assert theItem.itemRoot == "0123456789abc"
# Order
theItem.setOrder(None)
assert theItem.itemOrder == 0
@@ -208,6 +220,7 @@ def testCoreItem_Methods(mockGUI):
# Status + Icon
# =============
theItem.setType("FILE")
theItem.setStatus("Note")
theItem.setImport("Minor")
@@ -217,11 +230,19 @@ def testCoreItem_Methods(mockGUI):
assert stT == "Note"
assert isinstance(stI, QIcon)
theItem.setImportStatus("Draft")
stT, stI = theItem.getImportStatus()
assert stT == "Draft"
theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus()
assert stT == "Minor"
assert isinstance(stI, QIcon)
theItem.setImportStatus("Major")
stT, stI = theItem.getImportStatus()
assert stT == "Major"
# Representation
# ==============
@@ -263,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI):
assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH
# Alternative
theItem.setType(nwItemType.ROOT)
assert theItem.itemType == nwItemType.ROOT
@@ -282,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI):
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
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is True
theItem.setClass("NOVEL")
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is False
theItem.setClass("PLOT")
assert theItem.itemClass == nwItemClass.PLOT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CHARACTER")
assert theItem.itemClass == nwItemClass.CHARACTER
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("WORLD")
assert theItem.itemClass == nwItemClass.WORLD
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("TIMELINE")
assert theItem.itemClass == nwItemClass.TIMELINE
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("OBJECT")
assert theItem.itemClass == nwItemClass.OBJECT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ENTITY")
assert theItem.itemClass == nwItemClass.ENTITY
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CUSTOM")
assert theItem.itemClass == nwItemClass.CUSTOM
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ARCHIVE")
assert theItem.itemClass == nwItemClass.ARCHIVE
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is True
theItem.setClass("TRASH")
assert theItem.itemClass == nwItemClass.TRASH
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is True
assert theItem.isInactive() is True
# Alternative
theItem.setClass(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
@@ -332,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI):
theItem.setLayout("NOTE")
assert theItem.itemLayout == nwItemLayout.NOTE
# Alternatives
# Alternative
theItem.setLayout(nwItemLayout.NOTE)
assert theItem.itemLayout == nwItemLayout.NOTE
# END Test testCoreItem_LayoutSetter
@pytest.mark.core
def testCoreItem_ClassDefaults(mockGUI):
"""Test the setter for the default values.
"""
theProject = NWProject(mockGUI)
theItem = NWItem(theProject)
# Root items should not have their class updated
theItem.setParent(None)
theItem.setClass(nwItemClass.NO_CLASS)
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NO_CLASS
# Non-root items should have their class updated
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
# Non-layout items should have their layout set based on class
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.NO_LAYOUT)
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.NO_LAYOUT)
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
theItem.setClassDefaults(nwItemClass.PLOT)
assert theItem.itemLayout == nwItemLayout.NOTE
# If documents are not allowed in that class, the layout should be changed
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.DOCUMENT)
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setClassDefaults(nwItemClass.PLOT)
assert theItem.itemLayout == nwItemLayout.NOTE
# In all cases, status and importance should no longer be None
assert theItem.itemStatus is not None
assert theItem.itemImport is not None
# END Test testCoreItem_ClassDefaults
@pytest.mark.core
def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
"""Test packing and unpacking XML objects for the NWItem class.
@@ -353,6 +478,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
@@ -370,9 +496,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" order="1" type="FILE" class="NOVEL" '
b'layout="NOTE"><meta charCount="7" wordCount="5" paraCount="3" cursorPos="11"/>'
b'<name status="None" import="%s" exported="False">A Name</name></item>'
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'type="FILE" class="NOVEL" layout="NOTE"><meta charCount="7" wordCount="5" paraCount="3" '
b'cursorPos="11"/><name status="None" import="%s" exported="False">A Name</name></item>'
b'</content>'
) % bytes(constData.importKeys[3], encoding="utf8")
@@ -381,6 +507,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExported is False
assert theItem.paraCount == 3
@@ -399,6 +526,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
@@ -417,9 +545,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" order="1" type="FOLDER" '
b'class="NOVEL"><meta expanded="True"/><name status="%s" import="None">A Name</name>'
b'</item>'
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="%s" '
b'import="None">A Name</name></item>'
b'</content>'
) % bytes(constData.statusKeys[1], encoding="utf8")
@@ -428,6 +556,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded is True
assert theItem.isExported is True
+17 -10
View File
@@ -276,10 +276,10 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str)
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str)
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str)
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str)
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
@@ -314,8 +314,8 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
assert isinstance(theProject.newFile("Hello", "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str)
assert theProject.projChanged
assert theProject.saveProject() is True
assert theProject.closeProject() is True
@@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
theProject.projTree._treeOrder.append("01234567789abc")
# Add an item with a non-existent parent
nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a")
nHandle = theProject.newFile("Test File", "a6d311a93600a")
theProject.projTree[nHandle].setParent("cba9876543210")
assert theProject.projTree[nHandle].itemParent == "cba9876543210"
@@ -740,7 +740,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData):
# Change Importance
# =================
fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568")
fHandle = theProject.newFile("Jane Doe", "73475cb40a568")
theProject.projTree[fHandle].setImport("Main")
assert theProject.projTree[fHandle].itemImport == constData.importKeys[3]
@@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
"""
theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum)
assert theProject.openProject(nwLipsum) is True
assert theProject.projTree["636b6aa9b697b"] is None
assert theProject.closeProject()
# Add a file with non-existent parent
# This file will be renoved from the project on open
assert theProject.newFile("Oops", "0000000000000")
# Save and close
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# First Item with Meta Data
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
+104 -52
View File
@@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
import random
from lxml import etree
from hashlib import sha256
@@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles
def mockItems(mockGUI):
"""Create a list of mock items.
"""
random.seed(42)
theProject = NWProject(mockGUI)
itemA = NWItem(theProject)
@@ -103,7 +105,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE),
("a000000000003", None, itemF),
("a000000000004", None, itemG),
("b000000000002", "a000000000002", itemH),
("b000000000002", "a000000000004", itemH),
]
return theItems
@@ -120,22 +122,21 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__)
assert not theTree
assert bool(theTree) is False
# Check for archive and trash folders
assert theTree.trashRoot() is None
assert theTree.archiveRoot() is None
assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle)
assert theTree.append(tHandle, pHandle, nwItem)
assert theTree.append(tHandle, pHandle, nwItem) is True
assert theTree.updateItemData(tHandle) is True
assert theTree._treeChanged
assert theTree._treeChanged is True
# Check that tree is not empty (calls __bool__)
assert theTree
assert bool(theTree) is True
# Check the number of elements (calls __len__)
assert len(theTree) == len(mockItems)
@@ -149,8 +150,29 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002"
assert theTree.isTrashRoot("a000000000003")
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrash("a000000000003") is True
assert theTree.isRoot("a000000000002") is True
# Check the isTrash function
assert theTree.isTrash("0000000000000") is True # Doesn't exist
assert theTree.isTrash("a000000000003") is True # This the trash folder
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
assert theTree.isTrash("a000000000003") is True # This is still trash
theTree["a000000000003"].setClass(nwItemClass.TRASH)
assert theTree.isTrash("b000000000002") is False # This is not trash
value = theTree["b000000000002"].itemParent
theTree["b000000000002"].setParent("a000000000003")
assert theTree.isTrash("b000000000002") is True # This is in trash
theTree["b000000000002"].setParent(value)
value = theTree["b000000000002"].itemRoot
theTree["b000000000002"].setRoot("a000000000003")
assert theTree.isTrash("b000000000002") is True # This is in trash
theTree["b000000000002"].setRoot(value)
# Try to add another trash folder
itemT = NWItem(theProject)
@@ -159,7 +181,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.TRASH
itemT._expanded = False
assert not theTree.append("1234567890abc", None, itemT)
assert theTree.append("1234567890abc", None, itemT) is False
assert len(theTree) == len(mockItems)
# Generate handle automatically
@@ -169,14 +191,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.NOVEL
itemT._layout = nwItemLayout.DOCUMENT
assert theTree.append(None, None, itemT)
assert theTree.append(None, None, itemT) is True
assert theTree.updateItemData(itemT.itemHandle) is True
assert len(theTree) == len(mockItems) + 1
theList = theTree.handles()
assert theList[-1] == "73475cb40a568"
# Try to add existing handle
assert not theTree.append("73475cb40a568", None, itemT)
assert theTree.append("73475cb40a568", None, itemT) is False
assert len(theTree) == len(mockItems) + 1
# Delete a non-existing item
@@ -196,7 +219,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000002"]
assert len(theTree) == len(mockItems) - 2
assert "a000000000002" not in theTree
assert theTree.archiveRoot() is None
del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3
@@ -215,31 +237,43 @@ def testCoreTree_Methods(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
# Update item data, nonsense handle
assert theTree.updateItemData("stuff") is False
# Update item data, invalid item parent
corrParent = theTree["b000000000001"].itemParent
theTree["b000000000001"].setParent("0000000000000")
assert theTree.updateItemData("b000000000001") is False
# Update item data, valid item parent
theTree["b000000000001"].setParent(corrParent)
assert theTree.updateItemData("b000000000001") is True
# Update item data, root is unreachable
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0
with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001")
theTree.MAX_DEPTH = maxDepth
# Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
assert theTree.checkType("c000000000001", nwItemType.FILE) is True
# Root item lookup
theTree._treeRoots.append("stuff")
assert theTree.findRoot(nwItemClass.WORLD) is None
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
# Check for root uniqueness
assert theTree.checkRootUnique(nwItemClass.CUSTOM)
assert theTree.checkRootUnique(nwItemClass.WORLD)
assert not theTree.checkRootUnique(nwItemClass.NOVEL)
assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
# Find root item of child item
assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
assert theTree.getRootItem("stuff") is None
# Add a fake item to root and check that it can handle it
theTree._treeRoots["0000000000000"] = NWItem(theProject)
assert theTree.findRoot(nwItemClass.WORLD) is None
del theTree._treeRoots["0000000000000"]
# Get item path
assert theTree.getItemPath("stuff") == []
@@ -247,6 +281,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001"
]
# Cause recursion error
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0
with pytest.raises(RecursionError):
theTree.getItemPath("c000000000001")
theTree.MAX_DEPTH = maxDepth
# Break the folder parent handle
theTree["b000000000001"]._parent = "stuff"
assert theTree.getItemPath("c000000000001") == [
@@ -371,7 +412,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core
def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData):
"""Test packing and unpacking the tree to and from XML.
"""
theProject = NWProject(mockGUI)
@@ -379,37 +420,47 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
nwXML = etree.Element("novelWriterXML")
theTree.packXML(nwXML)
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
b'<novelWriterXML>'
b'<content count="8">'
b'<item handle="a000000000001" parent="None" order="0" type="ROOT" class="NOVEL"><meta '
b'expanded="True"/><name status="None" import="None">Novel</name></item>'
b'<item handle="b000000000001" parent="a000000000001" order="0" type="FOLDER" '
b'class="NOVEL"><meta expanded="True"/><name status="None" import="None">Act One</name>'
b'</item>'
b'<item handle="c000000000001" parent="b000000000001" order="0" type="FILE" class="NOVEL" '
b'layout="DOCUMENT"><meta charCount="300" wordCount="50" paraCount="2" cursorPos="0"/>'
b'<name status="None" import="None" exported="True">Chapter One</name></item>'
b'<item handle="c000000000002" parent="b000000000001" order="0" type="FILE" class="NOVEL" '
b'layout="DOCUMENT"><meta charCount="3000" wordCount="500" paraCount="20" cursorPos="0"/>'
b'<name status="None" import="None" exported="True">Scene One</name></item>'
b'<item handle="a000000000002" parent="None" order="0" type="ROOT" class="ARCHIVE"><meta '
b'expanded="False"/><name status="None" import="None">Outtakes</name></item>'
b'<item handle="a000000000003" parent="None" order="0" type="TRASH" class="TRASH"><meta '
b'expanded="False"/><name status="None" import="None">Trash</name></item>'
b'<item handle="a000000000004" parent="None" order="0" type="ROOT" class="CHARACTER">'
b'<meta expanded="True"/><name status="None" import="None">Characters</name></item>'
b'<item handle="b000000000002" parent="a000000000002" order="0" type="FILE" '
b'class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" paraCount="16" '
b'cursorPos="0"/><name status="None" import="None" exported="True">Jane Doe</name></item>'
b'</content>'
b'</novelWriterXML>'
)
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes((
'<novelWriterXML>'
'<content count="8">'
'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
'class="NOVEL"><meta expanded="True"/><name status="{s0}" '
'import="{i0}">Novel</name></item>'
'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="{s0}" '
'import="{i0}">Act One</name></item>'
'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" '
'paraCount="2" cursorPos="0"/><name status="{s0}" import="{i0}" '
'exported="True">Chapter One</name></item>'
'<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="3000" wordCount="500" '
'paraCount="20" cursorPos="0"/><name status="{s0}" import="{i0}" '
'exported="True">Scene One</name></item>'
'<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
'class="ARCHIVE"><meta expanded="False"/><name status="{s0}" '
'import="{i0}">Outtakes</name></item>'
'<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="TRASH" '
'class="TRASH"><meta expanded="False"/><name status="{s0}" '
'import="{i0}">Trash</name></item>'
'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
'class="CHARACTER"><meta expanded="True"/><name status="{s0}" '
'import="{i0}">Characters</name></item>'
'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
'type="FILE" class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" '
'paraCount="16" cursorPos="0"/><name status="{s0}" import="{i0}" '
'exported="True">Jane Doe</name></item>'
'</content>'
'</novelWriterXML>'
).format(
s0=constData.statusKeys[0], i0=constData.importKeys[0]
), encoding="utf8")
theTree.clear()
assert len(theTree) == 0
@@ -429,6 +480,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff")
+3 -3
View File
@@ -58,9 +58,9 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True
+1 -7
View File
@@ -62,7 +62,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True
@@ -230,12 +230,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwSplit._doSplit() is False
# Block folder creation by returning that the folder has a depth
# of 50 items in the tree
with monkeypatch.context() as mp:
mp.setattr(NWTree, "getItemPath", lambda *a: [""]*50)
assert nwSplit._doSplit() is False
# Clear the list
nwSplit.listBox.clear()
assert nwSplit._doSplit() is False
+1 -1
View File
@@ -175,7 +175,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData):
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New File"
assert itemEdit.editName.text() == "New Note"
assert itemEdit.editStatus.currentData() == constData.importKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True
+2 -2
View File
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.core import countWords
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout
from novelwriter.constants import nwKeyWords, nwUnicode
keyDelay = 2
@@ -1143,7 +1143,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
# Create Character
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n"
cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3")
assert nwGUI.openDocument(cHandle) is True
assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.saveDocument() is True
+376 -180
View File
@@ -22,9 +22,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
import os
from tools import writeFile
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.guimain import GuiMain
@@ -33,207 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass
@pytest.mark.gui
def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.treeView
##
# Add New Items
##
# Try to add item with no project
assert nwTree.newTreeItem(nwItemType.FILE) is False
# Try to add and move item with no project
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.moveTreeItem(1)
# Create a project
nwGUI.theProject.projTree.setSeed(42)
prjDir = os.path.join(fncDir, "project")
assert nwGUI.newProject({"projPath": prjDir}) is True
# Open a project
assert nwGUI.openProject(nwMinimal)
# No itemType set
nwTree.clearSelection()
assert nwTree.newTreeItem(None) is False
# Root Items
# ==========
# No class set
assert nwTree.newTreeItem(nwItemType.ROOT) is False
# Create root item
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
assert "1a6562590ef19" in nwGUI.theProject.projTree
# File/Folder Items
# =================
# No location selected for new item
nwTree.clearSelection()
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
caplog.clear()
assert nwTree.newTreeItem(nwItemType.FILE) is False
assert nwTree.newTreeItem(nwItemType.FOLDER) is False
assert "Did not find anywhere" in caplog.text
# No itemType set or ROOT, but no class
assert not nwTree.newTreeItem(None, None)
assert not nwTree.newTreeItem(nwItemType.ROOT, None)
# Create new folder as child of Novel folder
nwTree.setSelectedHandle("73475cb40a568")
assert nwTree.newTreeItem(nwItemType.FOLDER) is True
assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568"
assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568"
assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL
# Select a location
chItem = nwTree._getTreeItem("a6d311a93600a")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
chItem.setExpanded(True)
# Add a new file in the new folder
nwTree.setSelectedHandle("031b4af5197ec")
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec"
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568"
assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL
# Create new item with no class set (defaults to NOVEL)
assert nwTree.newTreeItem(nwItemType.FILE, None)
assert nwTree.newTreeItem(nwItemType.FOLDER, None)
# Add a new file next to the other new file
nwTree.setSelectedHandle("41cfc0d1f2d12")
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec"
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568"
assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("2858dcd1057d3")
assert nwGUI.docEditor.getText() == "### New Document\n\n"
# Check that we have the correct tree order
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
# Add a new file to the characters folder
nwTree.setSelectedHandle("71ee45a3c0db9")
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9"
assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9"
assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER
assert nwGUI.openDocument("2fca346db6561")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
# Add roots
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
# Make sure the sibling folder bug trap works
nwTree.setSelectedHandle("2858dcd1057d3")
nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen
caplog.clear()
assert nwTree.newTreeItem(nwItemType.FILE) is False
assert "Internal error" in caplog.text
nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec")
# Change max depth and try to add a subfolder that is too deep
monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2)
chItem = nwTree._getTreeItem("71ee45a3c0db9")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
# Get the trash folder
nwTree._addTrashRoot()
trashHandle = nwGUI.theProject.trashFolder()
nwTree.setSelectedHandle(trashHandle)
assert nwTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text
##
# Move Items
##
# Other Checks
# ============
nwTree.setSelectedHandle("8c659a11cd429")
# Also check error handling in reveal function
assert nwTree.revealNewTreeItem("abc") is False
# Shift focus and try to move item
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
assert not nwTree.moveTreeItem(1)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Move second item up twice (should give same result)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
# Move it back down four times (last two should be the same)
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move up twice, and undo
nwTree._lastMove = {}
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move a root item (top level items are different) twice
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
nwTree.setSelectedHandle("9d5247ab588e0")
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
##
# Delete and Trash
##
# Add some content to the new file
nwGUI.openDocument("73475cb40a568")
nwGUI.docEditor.setText("# Hello World\n")
nwGUI.saveDocument()
nwGUI.saveProject()
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
# Delete the items we added earlier
nwTree.clearSelection()
assert not nwTree.emptyTrash() # No folder yet
assert not nwTree.deleteItem(None)
assert not nwTree.deleteItem("1111111111111")
assert nwTree.deleteItem("73475cb40a568") # New File
assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
assert nwTree.deleteItem("811786ad1ae74") # Custom Root
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
# The file is in trash, empty it
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert nwTree.emptyTrash()
assert not nwTree.emptyTrash() # Already empty
assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
# Should not be allowed to add files and folders to Trash
trashHandle = nwGUI.theProject.projTree.trashRoot()
chItem = nwTree._getTreeItem(trashHandle)
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
# Close the project
nwGUI.closeProject()
##
# Orphaned Files
##
# Add an orphaned file
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
writeFile(orphFile, "# Hello World\n")
# Open the project again
nwGUI.openProject(nwMinimal)
# Check that the orphaned file was found and added to the tree
nwTree.flushTreeOrder()
assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
orItem = nwTree._getTreeItem("1234567890abc")
assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
##
# Unexpected Error Handling
##
# Add an item with an invalid type
assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL)
assert "Failed to add new item" in caplog.messages[-1]
# Add new file after one that has no parent handle
chItem = nwTree._getTreeItem("44cb730c42048")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
nwTree.theProject.projTree["44cb730c42048"]._parent = None
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
nwTree.clearSelection()
# Add a file with no parent, and fail to find a suitable parent item
monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None)
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
# Add an item that cannot be displayed in the tree
nHandle = nwGUI.theProject.newFile("Test", None)
assert nwTree.revealNewTreeItem(nHandle) is False
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_TreeItems
# END Test testGuiProjTree_NewItems
@pytest.mark.gui
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
nwTree = nwGUI.treeView
# Try to move item with no project
assert nwTree.moveTreeItem(1) is False
# Create a project
nwGUI.theProject.projTree.setSeed(42)
prjDir = os.path.join(fncDir, "project")
assert nwGUI.newProject({"projPath": prjDir}) is True
# Move Documents
# ==============
# Add some files
nwTree.setSelectedHandle("31489056e0916")
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Move item without focus
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
assert nwTree.moveTreeItem(1) is False
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Move with no selections
nwTree.clearSelection()
assert nwTree.moveTreeItem(1) is False
# Move second item up twice (should give same result)
nwTree.setSelectedHandle("0e17daca5f3e1")
assert nwTree.moveTreeItem(-1) is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
assert nwTree.moveTreeItem(-1) is False
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Restore via menu entry
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Move fifth item down twice (should give same result)
nwTree.setSelectedHandle("031b4af5197ec")
assert nwTree.moveTreeItem(1) is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
]
assert nwTree.moveTreeItem(1) is False
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
]
# Restore via menu entry
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Move down again, and restore via undo
nwTree.setSelectedHandle("031b4af5197ec")
assert nwTree.moveTreeItem(1) is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
]
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Root Folder
# ===========
nwTree.setSelectedHandle("73475cb40a568")
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
# Move novel folder up
assert nwTree.moveTreeItem(-1) is False
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
# Move novel folder down
assert nwTree.moveTreeItem(1) is True
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1
# Move novel folder up again
assert nwTree.moveTreeItem(-1) is True
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_MoveItems
@pytest.mark.gui
def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
nwTree = nwGUI.treeView
# Try to run with no project
assert nwTree.emptyTrash() is False
assert nwTree.deleteItem() is False
# Create a project
nwGUI.theProject.projTree.setSeed(42)
prjDir = os.path.join(fncDir, "project")
assert nwGUI.newProject({"projPath": prjDir}) is True
# Try emptying the trash already now, when there is no trash folder
assert nwTree.emptyTrash() is False
# Add some files
nwTree.setSelectedHandle("31489056e0916")
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
]
# Delete File
# ===========
# Delete item without focus -> blocked
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
nwTree.setSelectedHandle("41cfc0d1f2d12")
caplog.clear()
assert nwTree.deleteItem() is False
assert "blocked" in caplog.text
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# No selection made
nwTree.clearSelection()
caplog.clear()
assert nwTree.deleteItem() is False
assert "no item to delete" in caplog.text
# Not a valid handle
nwTree.clearSelection()
caplog.clear()
assert nwTree.deleteItem("0000000000000") is False
assert "Could not find tree item" in caplog.text
# Block adding trash folder
funcPointer = nwTree._addTrashRoot
nwTree._addTrashRoot = lambda *a: None
assert nwTree.deleteItem("41cfc0d1f2d12") is False
nwTree._addTrashRoot = funcPointer
# Delete last two documents, which also adds the trash folder
assert nwTree.deleteItem("41cfc0d1f2d12") is True
assert nwTree.deleteItem("031b4af5197ec") is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
"1a6562590ef19"
]
trashHandle = nwGUI.theProject.projTree.trashRoot()
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "41cfc0d1f2d12", "031b4af5197ec"
]
# Delete the first file again (permanent), and ask for permission
# Also open the document in the editor, which should trigger a close
assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
assert "41cfc0d1f2d12" in nwGUI.theProject.projTree
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.openDocument("41cfc0d1f2d12") is True
assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12"
assert nwTree.deleteItem("41cfc0d1f2d12") is True
assert nwGUI.docEditor.docHandle() is None
assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "031b4af5197ec"
]
# Try to delete the second document, but block the deletion
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False)
assert nwTree.deleteItem("031b4af5197ec") is False
# Delete proper, and skip asking for permission
assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
assert "031b4af5197ec" in nwGUI.theProject.projTree
assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True
assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
assert "031b4af5197ec" not in nwGUI.theProject.projTree
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
# Delete Folder/Root
# ==================
# Deleting non-empty folders is blocked
assert nwTree.deleteItem("31489056e0916") is False # Folder
assert nwTree.deleteItem("73475cb40a568") is False # Root
# Add a folder we can delete
nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root
assert nwTree.newTreeItem(nwItemType.FOLDER) is True
assert "2fca346db6561" in nwGUI.theProject.projTree
# Try to delete, but block parent item lookup
with monkeypatch.context() as mp:
mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None)
caplog.clear()
assert nwTree.deleteItem("2fca346db6561") is False
assert "Could not delete folder" in caplog.text
assert "2fca346db6561" in nwGUI.theProject.projTree
# Delete folder properly
assert nwTree.deleteItem("2fca346db6561") is True
assert "2fca346db6561" not in nwGUI.theProject.projTree
# Delete the Character root
assert nwTree.deleteItem("71ee45a3c0db9") is True
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree
# Empty Trash
# ===========
# Try to empty trash that is already empty
caplog.clear()
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
assert nwTree.emptyTrash() is False
assert "already empty" in caplog.text
# Move the two remaining scene documents to trash
assert nwTree.deleteItem("0e17daca5f3e1") is True
assert nwTree.deleteItem("1a6562590ef19") is True
assert nwTree.getTreeFromHandle("31489056e0916") == [
"31489056e0916", "98010bd9270f9"
]
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0e17daca5f3e1", "1a6562590ef19"
]
# Empty trash, but select no on question
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwTree.emptyTrash() is False
# Empty the trash proper
nwTree._setTreeChanged(False)
assert nwTree.emptyTrash() is True
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
assert nwTree._treeChanged is True
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_DeleteItems
+2 -2
View File
@@ -25,7 +25,7 @@ import pytest
from PyQt5.QtWidgets import QMessageBox
from novelwriter.core import NWDoc
from novelwriter.enum import nwItemClass, nwState
from novelwriter.enum import nwState
@pytest.mark.gui
@@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9")
cHandle = nwGUI.theProject.newFile("A Note", "71ee45a3c0db9")
newDoc = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.treeView.revealNewTreeItem(cHandle)