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 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): def trConst(tString):
@@ -42,31 +42,12 @@ class nwConst():
FMT_DSTAMP = "%Y-%m-%d" # Date only format FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits # Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# END Class nwConst # 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(): class nwRegEx():
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
+3 -8
View File
@@ -30,7 +30,7 @@ import logging
from time import time from time import time
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
@@ -208,8 +208,6 @@ class NWIndex():
text. text.
""" """
theItem = self.theProject.projTree[tHandle] theItem = self.theProject.projTree[tHandle]
theRoot = self.theProject.projTree.getRootItem(tHandle)
if theItem is None: if theItem is None:
logger.info("Not indexing unknown item '%s'", tHandle) logger.info("Not indexing unknown item '%s'", tHandle)
return False return False
@@ -229,11 +227,8 @@ class NWIndex():
if theItem.itemParent is None: if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle) logger.info("Not indexing orphaned item '%s'", tHandle)
return False return False
if self.theProject.projTree.isTrashRoot(theItem.itemParent): if theItem.isInactive():
logger.debug("Not indexing trash item '%s'", tHandle) logger.debug("Not indexing inactive item '%s'", tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.debug("Not indexing archived item '%s'", tHandle)
return False return False
itemClass = theItem.itemClass itemClass = theItem.itemClass
+100 -37
View File
@@ -31,7 +31,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import ( from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
) )
from novelwriter.constants import nwLabels, nwLists, trConst from novelwriter.constants import nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,6 +45,7 @@ class NWItem():
self._name = "" self._name = ""
self._handle = None self._handle = None
self._parent = None self._parent = None
self._root = None
self._order = 0 self._order = 0
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
self._class = nwItemClass.NO_CLASS self._class = nwItemClass.NO_CLASS
@@ -85,6 +86,10 @@ class NWItem():
def itemParent(self): def itemParent(self):
return self._parent return self._parent
@property
def itemRoot(self):
return self._root
@property @property
def itemOrder(self): def itemOrder(self):
return self._order return self._order
@@ -147,6 +152,7 @@ class NWItem():
itemAttrib = {} itemAttrib = {}
itemAttrib["handle"] = str(self._handle) itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent) itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order) itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name) itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name) itemAttrib["class"] = str(self._class.name)
@@ -188,6 +194,7 @@ class NWItem():
return False return False
self.setParent(xItem.attrib.get("parent", None)) self.setParent(xItem.attrib.get("parent", None))
self.setRoot(xItem.attrib.get("root", None))
self.setOrder(xItem.attrib.get("order", 0)) self.setOrder(xItem.attrib.get("order", 0))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
@@ -249,7 +256,7 @@ class NWItem():
return return
## ##
# Methods # Lookup Methods
## ##
def describeMe(self, hLevel=None): def describeMe(self, hLevel=None):
@@ -275,11 +282,26 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) 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): def getImportStatus(self):
"""Return the relevant importance or status label and icon for """Return the relevant importance or status label and icon for
the current item based on its class. the current item based on its class.
""" """
if self._class in nwLists.CLS_NOVEL: if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status) stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status) stIcon = self.theProject.statusItems.icon(self._status)
else: else:
@@ -287,14 +309,44 @@ class NWItem():
stIcon = self.theProject.importItems.icon(self._import) stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon return stName, stIcon
def setImportStatus(self, theLabel): ##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is """Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport. a wrapper setter for setStatus and setImport.
""" """
if self._class in nwLists.CLS_NOVEL: if self.isNovelLike():
self.setStatus(theLabel) self.setStatus(value)
else: 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 return
## ##
@@ -310,26 +362,37 @@ class NWItem():
self._name = "" self._name = ""
return return
def setHandle(self, tHandle): def setHandle(self, handle):
"""Set the item handle, and ensure it is valid. """Set the item handle, and ensure it is valid.
""" """
if isHandle(tHandle): if isHandle(handle):
self._handle = tHandle self._handle = handle
else: else:
self._handle = None self._handle = None
return return
def setParent(self, pHandle): def setParent(self, handle):
"""Set the parent handle, and ensure it is valid. """Set the parent handle, and ensure it is valid.
""" """
if pHandle is None: if handle is None:
self._parent = None self._parent = None
elif isHandle(pHandle): elif isHandle(handle):
self._parent = pHandle self._parent = handle
else: else:
self._parent = None self._parent = None
return 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): def setOrder(self, order):
"""Set the item order, and ensure that it is valid. This value """Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at is purely a meta value, and not actually used by novelWriter at
@@ -338,59 +401,59 @@ class NWItem():
self._order = checkInt(order, 0) self._order = checkInt(order, 0)
return return
def setType(self, itemType): def setType(self, value):
"""Set the item type from either a proper nwItemType, or set it """Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType. from a string representing an nwItemType.
""" """
if isinstance(itemType, nwItemType): if isinstance(value, nwItemType):
self._type = itemType self._type = value
elif isItemType(itemType): elif isItemType(value):
self._type = nwItemType[itemType] self._type = nwItemType[value]
else: else:
logger.error("Unrecognised item type '%s'", itemType) logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
return return
def setClass(self, itemClass): def setClass(self, value):
"""Set the item class from either a proper nwItemClass, or set """Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass. it from a string representing an nwItemClass.
""" """
if isinstance(itemClass, nwItemClass): if isinstance(value, nwItemClass):
self._class = itemClass self._class = value
elif isItemClass(itemClass): elif isItemClass(value):
self._class = nwItemClass[itemClass] self._class = nwItemClass[value]
else: else:
logger.error("Unrecognised item class '%s'", itemClass) logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS self._class = nwItemClass.NO_CLASS
return return
def setLayout(self, itemLayout): def setLayout(self, value):
"""Set the item layout from either a proper nwItemLayout, or set """Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout. it from a string representing an nwItemLayout.
""" """
if isinstance(itemLayout, nwItemLayout): if isinstance(value, nwItemLayout):
self._layout = itemLayout self._layout = value
elif isItemLayout(itemLayout): elif isItemLayout(value):
self._layout = nwItemLayout[itemLayout] self._layout = nwItemLayout[value]
elif itemLayout in nwLists.DEP_LAYOUT: elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT self._layout = nwItemLayout.DOCUMENT
else: else:
logger.error("Unrecognised item layout '%s'", itemLayout) logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT self._layout = nwItemLayout.NO_LAYOUT
return return
def setStatus(self, itemStatus): def setStatus(self, value):
"""Set the item status by looking it up in the valid status """Set the item status by looking it up in the valid status
items of the current project. items of the current project.
""" """
self._status = self.theProject.statusItems.check(itemStatus) self._status = self.theProject.statusItems.check(value)
return return
def setImport(self, itemImport): def setImport(self, value):
"""Set the item importance by looking it up in the valid import """Set the item importance by looking it up in the valid import
items of the current project. items of the current project.
""" """
self._import = self.theProject.importItems.check(itemImport) self._import = self.theProject.importItems.check(value)
return return
def setExpanded(self, state): def setExpanded(self, state):
+33 -37
View File
@@ -46,14 +46,14 @@ from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp, checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt, simplified makeFileNameSafe, hexToInt, simplified
) )
from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWProject(): class NWProject():
FILE_VERSION = "1.4" FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent): def __init__(self, theParent):
@@ -121,46 +121,34 @@ class NWProject():
## ##
def newRoot(self, rootName, rootClass): def newRoot(self, rootName, rootClass):
"""Add a new root item. These items are unique, except for item class """Add a new root item.
CUSTOM, and always have parent handle set to None.
""" """
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(rootName) newItem.setName(rootName)
newItem.setType(nwItemType.ROOT) newItem.setType(nwItemType.ROOT)
newItem.setClass(rootClass) newItem.setClass(rootClass)
newItem.setStatus(0)
self.projTree.append(None, None, newItem) self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle): def newFolder(self, folderName, pHandle):
"""Add a new folder with a given name and class and parent item. """Add a new folder with a given name and parent item.
""" """
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(folderName) newItem.setName(folderName)
newItem.setType(nwItemType.FOLDER) newItem.setType(nwItemType.FOLDER)
newItem.setClass(folderClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem) self.projTree.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle): def newFile(self, fileName, pHandle):
"""Add a new file with a given name and class, and set a layout """Add a new file with a given name and parent item.
based on the class. DOCUMENT for NOVEL, otherwise NOTE.
""" """
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(fileName) newItem.setName(fileName)
newItem.setType(nwItemType.FILE) 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.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def trashFolder(self): def trashFolder(self):
@@ -173,6 +161,7 @@ class NWProject():
newItem.setType(nwItemType.TRASH) newItem.setType(nwItemType.TRASH)
newItem.setClass(nwItemClass.TRASH) newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem) self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
return trashHandle return trashHandle
@@ -287,10 +276,10 @@ class NWProject():
xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT) xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT)
xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER) xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER)
xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD) xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD)
xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1]) xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1])
xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1]) xHandle[6] = self.newFolder(self.tr("New Chapter"), xHandle[1])
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6]) xHandle[7] = self.newFile(self.tr("New Chapter"), xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6]) xHandle[8] = self.newFile(self.tr("New Scene"), xHandle[6])
aDoc = NWDoc(self, xHandle[5]) aDoc = NWDoc(self, xHandle[5])
aDoc.writeDocument(titlePage) aDoc.writeDocument(titlePage)
@@ -313,8 +302,7 @@ class NWProject():
self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot)
# Create a title page # Create a title page
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle) tHandle = self.newFile(self.tr("Title Page"), nHandle)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, tHandle) aDoc = NWDoc(self, tHandle)
aDoc.writeDocument(titlePage) aDoc.writeDocument(titlePage)
@@ -330,10 +318,9 @@ class NWProject():
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
pHandle = nHandle pHandle = nHandle
if chFolders: if chFolders:
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle) pHandle = self.newFolder(chTitle, nHandle)
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle) cHandle = self.newFile(chTitle, pHandle)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, cHandle) aDoc = NWDoc(self, cHandle)
aDoc.writeDocument("## %s\n\n" % chTitle) aDoc.writeDocument("## %s\n\n" % chTitle)
@@ -342,7 +329,7 @@ class NWProject():
if numScenes > 0: if numScenes > 0:
for sc in range(numScenes): for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}") 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 = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle) aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -351,7 +338,7 @@ class NWProject():
elif numScenes > 0: elif numScenes > 0:
for sc in range(numScenes): for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") 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 = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle) aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -481,8 +468,9 @@ class NWProject():
# documents and one for project notes. Introduced in # documents and one for project notes. Introduced in
# version 1.5. # version 1.5.
# 1.4 : Introduces a more compact format for storing items. All # 1.4 : Introduces a more compact format for storing items. All
# settings aside from name are now attributes. Introduced # settings aside from name are now attributes. This format
# in version 1.7. # 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"): if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
@@ -613,7 +601,13 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache() 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._scanProjectFolder()
self._loadProjectLocalisation() self._loadProjectLocalisation()
@@ -624,6 +618,7 @@ class NWProject():
self._writeLockFile() self._writeLockFile()
self.setProjectChanged(False) self.setProjectChanged(False)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True return True
@@ -1202,7 +1197,7 @@ class NWProject():
self.statusItems.resetCounts() self.statusItems.resetCounts()
self.importItems.resetCounts() self.importItems.resetCounts()
for nwItem in self.projTree: for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL: if nwItem.isNovelLike():
self.statusItems.increment(nwItem.itemStatus) self.statusItems.increment(nwItem.itemStatus)
else: else:
self.importItems.increment(nwItem.itemImport) self.importItems.increment(nwItem.itemImport)
@@ -1452,6 +1447,7 @@ class NWProject():
orphItem.setClass(oClass) orphItem.setClass(oClass)
orphItem.setLayout(oLayout) orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem) self.projTree.append(oHandle, oParent, orphItem)
self.projTree.updateItemData(orphItem.itemHandle)
if noWhere: if noWhere:
self.theParent.makeAlert(self.tr( 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.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkHandle from novelwriter.common import checkHandle
from novelwriter.constants import nwConst, nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,13 +41,15 @@ logger = logging.getLogger(__name__)
class NWTree(): class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
self._projTree = {} # Holds all the items of the project self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view 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._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index self._theIndex = 0 # The current iterator index
@@ -67,7 +69,7 @@ class NWTree():
""" """
self._projTree = {} self._projTree = {}
self._treeOrder = [] self._treeOrder = []
self._treeRoots = [] self._treeRoots = {}
self._trashRoot = None self._trashRoot = None
self._archRoot = None self._archRoot = None
self._theIndex = 0 self._theIndex = 0
@@ -98,7 +100,7 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT: if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle)) logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle) self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE: if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle)) logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle self._archRoot = tHandle
@@ -207,9 +209,30 @@ class NWTree():
return novelWords, noteWords 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): def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type. """Return true of item exists and is of the specified item type.
""" """
@@ -218,71 +241,6 @@ class NWTree():
return False return False
return tItem.itemType == itemType 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): def getItemPath(self, tHandle):
"""Iterate upwards in the tree until we find the item with """Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles. parent None, the root item, and return the list of handles.
@@ -293,7 +251,7 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1): for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
@@ -303,8 +261,56 @@ class NWTree():
return tTree return tTree
else: else:
tTree.append(tHandle) tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
return tTree 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 # Setters
## ##
@@ -420,7 +426,7 @@ class NWTree():
return return
if tHandle in self._treeRoots: if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle) del self._treeRoots[tHandle]
if tHandle == self._trashRoot: if tHandle == self._trashRoot:
self._trashRoot = None self._trashRoot = None
if tHandle == self._archRoot: if tHandle == self._archRoot:
+1 -1
View File
@@ -130,7 +130,7 @@ class GuiDocMerge(QDialog):
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False 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 = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport) newItem.setImport(srcItem.itemImport)
+3 -20
View File
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwAlert, nwItemType
from novelwriter.constants import nwConst
from novelwriter.gui.custom import QHelpLabel from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -160,16 +159,6 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False 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( msgYes = self.theParent.askQuestion(
self.tr("Split Document"), self.tr("Split Document"),
"{0}<br><br>{1}".format( "{0}<br><br>{1}".format(
@@ -186,22 +175,16 @@ class GuiDocSplit(QDialog):
return False return False
# Create the folder # Create the folder
fHandle = self.theProject.newFolder( fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
self.theParent.treeView.revealNewTreeItem(fHandle) self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle) logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files # Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder: for wTitle, iStart, iEnd in finalOrder:
isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#").strip() wTitle = wTitle.lstrip("#").strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.projTree[nHandle] newItem = self.theProject.projTree[nHandle]
newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport) newItem.setImport(srcItem.itemImport)
logger.verbose( logger.verbose(
+3 -3
View File
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.enum import nwItemLayout, nwItemType 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 from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -74,7 +74,7 @@ class GuiItemEditor(QDialog):
# Item Status # Item Status
self.editStatus = QComboBox() self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd) self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL: if self.theItem.isNovelLike():
for key, entry in self.theProject.statusItems.items(): for key, entry in self.theProject.statusItems.items():
self.editStatus.addItem(entry["icon"], entry["name"], key) self.editStatus.addItem(entry["icon"], entry["name"], key)
@@ -95,7 +95,7 @@ class GuiItemEditor(QDialog):
self.editLayout.setMinimumWidth(mVd) self.editLayout.setMinimumWidth(mVd)
validLayouts = [] validLayouts = []
if self.theItem.itemType == nwItemType.FILE: 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.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE) validLayouts.append(nwItemLayout.NOTE)
else: else:
+2 -20
View File
@@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar):
return 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 # Update Menu on Settings Changed
## ##
@@ -215,7 +197,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder # Project > New Folder
self.aCreateFolder = QAction(self.tr("Create Folder"), self) self.aCreateFolder = QAction(self.tr("Create Folder"), self)
self.aCreateFolder.setShortcut("Ctrl+Shift+N") 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) self.projMenu.addAction(self.aCreateFolder)
# Project > Separator # Project > Separator
@@ -277,7 +259,7 @@ class GuiMainMenu(QMenuBar):
# Document > New # Document > New
self.aNewDoc = QAction(self.tr("New Document"), self) self.aNewDoc = QAction(self.tr("New Document"), self)
self.aNewDoc.setShortcut("Ctrl+N") 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) self.docuMenu.addAction(self.aNewDoc)
# Document > Open # Document > Open
+88 -163
View File
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert 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__) logger = logging.getLogger(__name__)
@@ -161,114 +161,64 @@ class GuiProjectTree(QTreeWidget):
self._timeChanged = 0 self._timeChanged = 0
return return
def newTreeItem(self, itemType, itemClass): def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType and """Add new item to the tree, with a given itemType (and
itemClass, and attach it to the selected handle. Also make sure itemClass if Root), and attach it to the selected handle. Also make
the item is added in a place it can be added, and that other 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. meta data is set correctly to ensure a valid project tree.
""" """
pHandle = self.getSelectedHandle()
nHandle = None
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
if not isinstance(itemType, nwItemType): nHandle = None
# This would indicate an internal bug tHandle = None
logger.error("No itemType provided")
return False
# The item needs to be assigned an item class, so one must be if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# 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 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( tHandle = self.theProject.newRoot(
trConst(nwLabels.CLASS_NAME[itemClass]), itemClass trConst(nwLabels.CLASS_NAME[itemClass]), itemClass
) )
if tHandle is None:
logger.error("No root item added")
return False
else: elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
# 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)
# If still nothing, give up sHandle = self.getSelectedHandle()
if pHandle is None: if sHandle is None or sHandle not in self.theProject.projTree:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!" "Did not find anywhere to add the file or folder!"
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
# Now check if the selected item is a file, in which case # If the selected item is a file, the new item will be a sibling
# the new file will be a sibling pItem = self.theProject.projTree[sHandle]
pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE: if pItem.itemType == nwItemType.FILE:
nHandle = pHandle nHandle = sHandle
pHandle = pItem.itemParent sHandle = pItem.itemParent
if sHandle is None:
logger.error("Internal error") # Bug
return False
# If we again have no home, give up if self.theProject.projTree.isTrash(sHandle):
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):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder." "Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
parTree = self.theProject.projTree.getItemPath(pHandle) # Add the file or folder
# If we're still here, add the file or folder
if itemType == nwItemType.FILE: 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: elif itemType == nwItemType.FOLDER:
if len(parTree) >= nwConst.MAX_DEPTH - 1: tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle)
# 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)
else: else:
logger.error("Failed to add new item") logger.error("Failed to add new item")
return False return False
# If there is no handle set, return here # If there is no handle set, return here. This is a bug
if tHandle is None: if tHandle is None: # pragma: no cover
return True return True
# Add the new item to the tree # Add the new item to the tree
@@ -280,13 +230,9 @@ class GuiProjectTree(QTreeWidget):
if nwItem.itemType != nwItemType.FILE: if nwItem.itemType != nwItemType.FILE:
return True 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) newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument() if not newDoc.readDocument():
if curTxt is None:
curTxt = ""
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.DOCUMENT: if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n" newText = f"### {nwItem.itemName}\n\n"
else: else:
@@ -310,6 +256,9 @@ class GuiProjectTree(QTreeWidget):
"""Reveal a newly added project item in the project tree. """Reveal a newly added project item in the project tree.
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
return False
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None: if trItem is None:
return False return False
@@ -495,8 +444,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item") logger.error("Could not delete item")
return False return False
pHandle = nwItemS.itemParent if self.theProject.projTree.isTrash(tHandle):
if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the # If the file is in the trash folder already, as the
# user if they want to permanently delete the file. # user if they want to permanently delete the file.
doPermanent = False doPermanent = False
@@ -513,13 +461,6 @@ class GuiProjectTree(QTreeWidget):
if doPermanent: if doPermanent:
logger.debug("Permanently deleting file with handle '%s'", tHandle) 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) delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument(): if not delDoc.deleteDocument():
self.theParent.makeAlert([ self.theParent.makeAlert([
@@ -527,6 +468,13 @@ class GuiProjectTree(QTreeWidget):
], nwAlert.ERROR) ], nwAlert.ERROR)
return False 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.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle) self._deleteTreeItem(tHandle)
self._setTreeChanged(True) self._setTreeChanged(True)
@@ -540,19 +488,13 @@ class GuiProjectTree(QTreeWidget):
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName), self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
) )
if msgYes: if msgYes:
if pHandle is None:
logger.warning("File has no parent item")
logger.debug("Moving file '%s' to trash", tHandle) logger.debug("Moving file '%s' to trash", tHandle)
self.propagateCount(tHandle, 0) self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex) trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC) trItemT.addChild(trItemC)
self._updateItemParent(tHandle) self._postItemMove(tHandle, wCount)
self.propagateCount(tHandle, wCount)
self.theIndex.deleteHandle(tHandle)
self._recordLastMove(trItemS, trItemP, tIndex) self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True) self._setTreeChanged(True)
@@ -562,6 +504,7 @@ class GuiProjectTree(QTreeWidget):
if trItemP is None: if trItemP is None:
logger.error("Could not delete folder") logger.error("Could not delete folder")
return False return False
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0: if trItemS.childCount() == 0:
trItemP.takeChild(tIndex) trItemP.takeChild(tIndex)
@@ -581,7 +524,6 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0: if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex) self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle) self._deleteTreeItem(tHandle)
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True) self._setTreeChanged(True)
else: else:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
@@ -634,7 +576,7 @@ class GuiProjectTree(QTreeWidget):
return return
def propagateCount(self, tHandle, theCount, nDepth=0): def propagateCount(self, tHandle, theCount):
"""Recursive function setting the word count for a given item, """Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating root item. This function is more efficient than recalculating
@@ -654,12 +596,13 @@ class GuiProjectTree(QTreeWidget):
return return
pCount = 0 pCount = 0
pHandle = None
for i in range(pItem.childCount()): for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole) pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "": if pHandle:
self.propagateCount(pHandle, pCount, nDepth+1) self.propagateCount(pHandle, pCount)
return return
@@ -714,9 +657,7 @@ class GuiProjectTree(QTreeWidget):
movItem = parItem.takeChild(srcIndex) movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem) dstItem.insertChild(dstIndex, movItem)
snItem = self.theProject.projTree[sHandle] self._postItemMove(sHandle, wCount)
dnItem = self.theProject.projTree[dHandle]
self._postItemMove(sHandle, snItem, dnItem, wCount)
self.clearSelection() self.clearSelection()
movItem.setSelected(True) movItem.setSelected(True)
@@ -848,29 +789,34 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None: if pItem is not None:
pIndex = pItem.indexOfChild(sItem) 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 isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE onFile = dnItem.itemType == nwItemType.FILE
inSame = snItem.itemRoot == dnItem.itemRoot
isSame = snItem.itemClass == dnItem.itemClass allowDrop = inSame or isFile
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 &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot: if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item '%s' accepted", sHandle) logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
self.propagateCount(sHandle, 0) self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent) QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, snItem, dnItem, wCount) self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex) self._recordLastMove(sItem, pItem, pIndex)
else: else:
theEvent.ignore()
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle) logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
theEvent.ignore()
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"The item cannot be moved to that location." "The item cannot be moved to that location."
), nwAlert.ERROR) ), nwAlert.ERROR)
@@ -881,40 +827,39 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions # Internal Functions
## ##
def _postItemMove(self, sHandle, snItem, dnItem, wCount): def _postItemMove(self, tHandle, wCount):
"""Run various maintenance tasks for a moved item. """Run various maintenance tasks for a moved item.
""" """
isFile = snItem.itemType == nwItemType.FILE trItemS = self._getTreeItem(tHandle)
isSame = snItem.itemClass == dnItem.itemClass nwItemS = self.theProject.projTree[tHandle]
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile 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, logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
# 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)
# The items dropped into archive or trash should be removed # The items dropped into archive or trash should be removed
# from the project index, for all other items, we rescan the # from the project index, for all other items, we rescan the
# file to ensure the index is up to date. # file to ensure the index is up to date.
if onFree: if nwItemS.isInactive():
self.theIndex.deleteHandle(sHandle) self.theIndex.deleteHandle(tHandle)
else: else:
self.theIndex.reIndexHandle(sHandle) self.theIndex.reIndexHandle(tHandle)
# Trigger dependent updates # Trigger dependent updates
self._setTreeChanged(True) self._setTreeChanged(True)
self._emitItemChange(sHandle) self._emitItemChange(tHandle)
return return True
def _getTreeItem(self, tHandle): def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle. """Returns the QTreeWidgetItem of a given item handle.
@@ -966,7 +911,6 @@ class GuiProjectTree(QTreeWidget):
if pHandle is None: if pHandle is None:
if nwItem.itemType == nwItemType.ROOT: if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
self.theParent.mainMenu.setAvailableRoot()
elif nwItem.itemType == nwItemType.TRASH: elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
else: else:
@@ -1015,25 +959,6 @@ class GuiProjectTree(QTreeWidget):
return trItem 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): def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project. """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): if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
if nwItem.itemClass == nwItemClass.NOVEL: if nwItem.isNovelLike():
self.novelItemChanged.emit() self.novelItemChanged.emit()
else: else:
self.noteItemChanged.emit() self.noteItemChanged.emit()
@@ -1182,7 +1107,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new file call to the project tree. """Forward the new file call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FILE, None) self.theTree.newTreeItem(nwItemType.FILE)
return return
@pyqtSlot() @pyqtSlot()
@@ -1190,7 +1115,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new folder call to the project tree. """Forward the new folder call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FOLDER, None) self.theTree.newTreeItem(nwItemType.FOLDER)
return return
@pyqtSlot() @pyqtSlot()
+1 -2
View File
@@ -55,7 +55,6 @@ from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState nwItemType, nwItemClass, nwAlert, nwWidget, nwState
) )
from novelwriter.common import getGuiItem, hexToInt from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwLists
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -848,7 +847,7 @@ class GuiMain(QMainWindow):
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.projTree[tHandle]
if tItem is None: if tItem is None:
return False return False
if tItem.itemType not in nwLists.REG_TYPES: if tItem.itemType == nwItemType.NO_TYPE:
return False return False
logger.verbose("Requesting change to item '%s'", tHandle) logger.verbose("Requesting change to item '%s'", tHandle)
+2 -8
View File
@@ -780,14 +780,12 @@ class GuiBuildNovel(QDialog):
if theItem is None: if theItem is None:
return False return False
if not theItem.isExported and not ignoreFlag: if not (theItem.isExported or ignoreFlag):
return False return False
isNone = theItem.itemType != nwItemType.FILE isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.isInactive()
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.itemParent is None isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote isNovel = not isNone and not isNote
@@ -799,10 +797,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles: if isNovel and not novelFiles:
return False return False
rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle)
if rootItem.itemClass == nwItemClass.ARCHIVE:
return False
return True return True
def _saveDocument(self, theFmt): def _saveDocument(self, theFmt):
+30 -30
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>1303</saveCount> <saveCount>1306</saveCount>
<autoCount>199</autoCount> <autoCount>199</autoCount>
<editTime>65049</editTime> <editTime>65149</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -33,7 +33,7 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <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="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="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> <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> <entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
</status> </status>
<importance> <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="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="i2d7a54" count="2" red="21" green="0" blue="180">Major</entry>
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry> <entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="25"> <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"/> <meta expanded="True"/>
<name status="sc24b8f" import="ia857f0">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
</item> </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"/> <meta charCount="93" wordCount="19" paraCount="2" cursorPos="2"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name> <name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
</item> </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"/> <meta charCount="186" wordCount="39" paraCount="2" cursorPos="212"/>
<name status="sf12341" import="ia857f0" exported="True">Page</name> <name status="sf12341" import="ia857f0" exported="True">Page</name>
</item> </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"/> <meta charCount="26" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="sf12341" import="ia857f0" exported="True">Part One</name> <name status="sf12341" import="ia857f0" exported="True">Part One</name>
</item> </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"/> <meta expanded="True"/>
<name status="s90e6c9" import="ia857f0">A Folder</name> <name status="s90e6c9" import="ia857f0">A Folder</name>
</item> </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"/> <meta charCount="75" wordCount="14" paraCount="1" cursorPos="279"/>
<name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name> <name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name>
</item> </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"/> <meta charCount="2429" wordCount="432" paraCount="14" cursorPos="219"/>
<name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name> <name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name>
</item> </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"/> <meta charCount="476" wordCount="93" paraCount="3" cursorPos="577"/>
<name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name> <name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name>
</item> </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"/> <meta charCount="617" wordCount="101" paraCount="3" cursorPos="4"/>
<name status="sf12341" import="ia857f0" exported="True">Interlude</name> <name status="sf12341" import="ia857f0" exported="True">Interlude</name>
</item> </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"/> <meta charCount="1692" wordCount="313" paraCount="6" cursorPos="1110"/>
<name status="sd51c5b" import="ia857f0" exported="False">A Note on Structure</name> <name status="sd51c5b" import="ia857f0" exported="False">A Note on Structure</name>
</item> </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"/> <meta charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name> <name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name>
</item> </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"/> <meta charCount="189" wordCount="37" paraCount="1" cursorPos="224"/>
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name> <name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Characters</name> <name status="sf12341" import="ia857f0">Characters</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Main Characters</name> <name status="sf12341" import="ia857f0">Main Characters</name>
</item> </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"/> <meta charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="sf12341" import="icfb3a5" exported="True">John Smith</name> <name status="sf12341" import="icfb3a5" exported="True">John Smith</name>
</item> </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"/> <meta charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name> <name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Locations</name> <name status="sf12341" import="ia857f0">Locations</name>
</item> </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"/> <meta charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="sf12341" import="i56be10" exported="True">Earth</name> <name status="sf12341" import="i56be10" exported="True">Earth</name>
</item> </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"/> <meta charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="sf12341" import="icfb3a5" exported="True">Space</name> <name status="sf12341" import="icfb3a5" exported="True">Space</name>
</item> </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"/> <meta charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" exported="True">Mars</name> <name status="sf12341" import="i2d7a54" exported="True">Mars</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Archive</name> <name status="sf12341" import="ia857f0">Archive</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Scenes</name> <name status="sf12341" import="ia857f0">Scenes</name>
</item> </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"/> <meta charCount="315" wordCount="55" paraCount="1" cursorPos="322"/>
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name> <name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
</item> </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"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Trash</name> <name status="sf12341" import="ia857f0">Trash</name>
</item> </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"/> <meta charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Delete Me!</name> <name status="sf12341" import="ia857f0" exported="True">Delete Me!</name>
</item> </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(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr("novelwriter.CONFIG", fncConf) 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) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.wait(20) qtbot.wait(20)
+21 -21
View File
@@ -44,87 +44,87 @@
</importance> </importance>
</settings> </settings>
<content count="21"> <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"/> <meta expanded="True"/>
<name status="sbaa94f" import="i613591">Novel</name> <name status="sbaa94f" import="i613591">Novel</name>
</item> </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"/> <meta charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name> <name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
</item> </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"/> <meta charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" exported="True">Front Matter</name> <name status="sedd043" import="i613591" exported="True">Front Matter</name>
</item> </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"/> <meta charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" exported="True">Prologue</name> <name status="s92a87b" import="i613591" exported="True">Prologue</name>
</item> </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"/> <meta charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" exported="True">Act One</name> <name status="sbaa94f" import="i613591" exported="True">Act One</name>
</item> </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"/> <meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter One</name> <name status="s92a87b" import="i613591">Chapter One</name>
</item> </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"/> <meta charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter One</name> <name status="s92a87b" import="i613591" exported="True">Chapter One</name>
</item> </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"/> <meta charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" exported="True">Scene One</name> <name status="sedd043" import="i613591" exported="True">Scene One</name>
</item> </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"/> <meta charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" exported="True">Scene Two</name> <name status="sedd043" import="i613591" exported="True">Scene Two</name>
</item> </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"/> <meta charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" exported="False">Interlude</name> <name status="sbaa94f" import="i613591" exported="False">Interlude</name>
</item> </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"/> <meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter Two</name> <name status="s92a87b" import="i613591">Chapter Two</name>
</item> </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"/> <meta charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name> <name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
</item> </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"/> <meta charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" exported="True">Scene Three</name> <name status="sedd043" import="i613591" exported="True">Scene Three</name>
</item> </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"/> <meta charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" exported="True">Scene Four</name> <name status="sedd043" import="i613591" exported="True">Scene Four</name>
</item> </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"/> <meta charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" exported="True">Scene Five</name> <name status="sedd043" import="i613591" exported="True">Scene Five</name>
</item> </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"/> <meta expanded="True"/>
<name status="sbaa94f" import="i613591">Characters</name> <name status="sbaa94f" import="i613591">Characters</name>
</item> </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"/> <meta charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name> <name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
</item> </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"/> <meta expanded="True"/>
<name status="sbaa94f" import="i613591">Plot</name> <name status="sbaa94f" import="i613591">Plot</name>
</item> </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"/> <meta charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" exported="True">Main</name> <name status="sbaa94f" import="i613591" exported="True">Main</name>
</item> </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"/> <meta expanded="True"/>
<name status="sbaa94f" import="i613591">World</name> <name status="sbaa94f" import="i613591">World</name>
</item> </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"/> <meta charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name> <name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
</item> </item>
+8 -8
View File
@@ -42,35 +42,35 @@
</importance> </importance>
</settings> </settings>
<content count="8"> <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"/> <meta expanded="True"/>
<name status="s72322d" import="iffcacb">Novel</name> <name status="s72322d" import="iffcacb">Novel</name>
</item> </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"/> <meta charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" exported="True">Title Page</name> <name status="s72322d" import="iffcacb" exported="True">Title Page</name>
</item> </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"/> <meta expanded="True"/>
<name status="s72322d" import="iffcacb">New Chapter</name> <name status="s72322d" import="iffcacb">New Chapter</name>
</item> </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"/> <meta charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name> <name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
</item> </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"/> <meta charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" exported="True">New Scene</name> <name status="s72322d" import="iffcacb" exported="True">New Scene</name>
</item> </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"/> <meta expanded="False"/>
<name status="s72322d" import="iffcacb">Plot</name> <name status="s72322d" import="iffcacb">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="s72322d" import="iffcacb">Characters</name> <name status="s72322d" import="iffcacb">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="s72322d" import="iffcacb">World</name> <name status="s72322d" import="iffcacb">World</name>
</item> </item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>Test Custom</name> <name>Test Custom</name>
<title>Test Novel</title> <title>Test Novel</title>
@@ -35,104 +35,104 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="23"> <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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Novel</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Plot</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Characters</name> <name status="sbc8960" import="ie465e1">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Locations</name> <name status="sbc8960" import="ie465e1">Locations</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name> <name status="sbc8960" import="ie465e1">Timeline</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Objects</name> <name status="sbc8960" import="ie465e1">Objects</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Entities</name> <name status="sbc8960" import="ie465e1">Entities</name>
</item> </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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 1</name> <name status="sbc8960" import="ie465e1">Chapter 1</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 2</name> <name status="sbc8960" import="ie465e1">Chapter 2</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Chapter 3</name> <name status="sbc8960" import="ie465e1">Chapter 3</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>Test Custom</name> <name>Test Custom</name>
<title>Test Novel</title> <title>Test Novel</title>
@@ -35,68 +35,68 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="14"> <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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Novel</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Plot</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Characters</name> <name status="sbc8960" import="ie465e1">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Locations</name> <name status="sbc8960" import="ie465e1">Locations</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name> <name status="sbc8960" import="ie465e1">Timeline</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Objects</name> <name status="sbc8960" import="ie465e1">Objects</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Entities</name> <name status="sbc8960" import="ie465e1">Entities</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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>
<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"/> <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> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -33,52 +33,52 @@
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="10"> <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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name> <name status="sbc8960" import="ie465e1">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name> <name status="sbc8960" import="ie465e1">World</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name> <name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name> <name status="sbc8960" import="ie465e1">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name> <name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name> <name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item> </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"/> <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>
<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"/> <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> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -40,35 +40,35 @@
</importance> </importance>
</settings> </settings>
<content count="8"> <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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name> <name status="sbc8960" import="ie465e1">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name> <name status="sbc8960" import="ie465e1">World</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name> <name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name> <name status="sbc8960" import="ie465e1">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name> <name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name> <name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item> </item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -27,66 +27,82 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <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="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="sad3c2d" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbd9c66" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i16419f" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i972a84" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="12"> <content count="16">
<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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Novel</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Plot</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">Characters</name> <name status="sbc8960" import="ie465e1">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">World</name> <name status="sbc8960" import="ie465e1">World</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">Title Page</name> <name status="sbc8960" import="ie465e1" exported="True">Title Page</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="ie465e1">New Chapter</name> <name status="sbc8960" import="ie465e1">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Chapter</name> <name status="sbc8960" import="ie465e1" exported="True">New Chapter</name>
</item> </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"/> <meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="sbc8960" import="ie465e1" exported="True">New Scene</name> <name status="sbc8960" import="ie465e1" exported="True">New Scene</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Timeline</name> <name status="sbc8960" import="ie465e1">Novel</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Object</name> <name status="sbc8960" import="ie465e1">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sbc8960" import="None">Custom1</name> <name status="sbc8960" import="ie465e1">Character</name>
</item> </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"/> <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> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 44cb730c42048/031b4af5197ec %%~path: 44cb730c42048/031b4af5197ec
%%~kind: PLOT/NOTE %%~kind: PLOT/NOTE
# Main Plot # Main Plot
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 71ee45a3c0db9/1a6562590ef19 %%~path: 71ee45a3c0db9/1a6562590ef19
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
# Jane Doe # Jane Doe
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 811786ad1ae74/41cfc0d1f2d12 %%~path: 811786ad1ae74/41cfc0d1f2d12
%%~kind: WORLD/NOTE %%~kind: WORLD/NOTE
# Main Location # Main Location
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -33,60 +33,60 @@
<entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="12"> <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"/> <meta expanded="True"/>
<name status="sa3b179" import="i466852">Novel</name> <name status="sa3b179" import="i466852">Novel</name>
</item> </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"/> <meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="i466852" exported="True">Title Page</name> <name status="sa3b179" import="i466852" exported="True">Title Page</name>
</item> </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"/> <meta expanded="True"/>
<name status="sa3b179" import="i466852">New Chapter</name> <name status="sa3b179" import="i466852">New Chapter</name>
</item> </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"/> <meta charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="sa3b179" import="i466852" exported="True">New Chapter</name> <name status="sa3b179" import="i466852" exported="True">New Chapter</name>
</item> </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"/> <meta charCount="612" wordCount="95" paraCount="10" cursorPos="768"/>
<name status="sa3b179" import="i466852" exported="True">New Scene</name> <name status="sa3b179" import="i466852" exported="True">New Scene</name>
</item> </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"/> <meta expanded="True"/>
<name status="sa3b179" import="i466852">Plot</name> <name status="sa3b179" import="i466852">Plot</name>
</item> </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"/> <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>
<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"/> <meta expanded="True"/>
<name status="sa3b179" import="i466852">Characters</name> <name status="sa3b179" import="i466852">Characters</name>
</item> </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"/> <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>
<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"/> <meta expanded="True"/>
<name status="sa3b179" import="i466852">World</name> <name status="sa3b179" import="i466852">World</name>
</item> </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"/> <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>
<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"/> <meta expanded="True"/>
<name status="None" import="None">Trash</name> <name status="sa3b179" import="i466852">Trash</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -33,44 +33,44 @@
<entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="sbdd640" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i23b8c1" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="8"> <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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Novel</name> <name status="sa3b179" import="i466852">Novel</name>
</item> </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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">New Chapter</name> <name status="sa3b179" import="i466852">New Chapter</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Plot</name> <name status="sa3b179" import="i466852">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Characters</name> <name status="sa3b179" import="i466852">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">World</name> <name status="sa3b179" import="i466852">World</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>Project Name</name> <name>Project Name</name>
<title>Project Title</title> <title>Project Title</title>
@@ -39,44 +39,44 @@
<entry key="sbc8960" count="0" red="20" green="30" blue="40">Final</entry> <entry key="sbc8960" count="0" red="20" green="30" blue="40">Final</entry>
</status> </status>
<importance> <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="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="i392456" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i1a3d1f" count="0" red="100" green="100" blue="100">Final</entry> <entry key="i1a3d1f" count="0" red="100" green="100" blue="100">Final</entry>
</importance> </importance>
</settings> </settings>
<content count="8"> <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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Novel</name> <name status="sa3b179" import="i466852">Novel</name>
</item> </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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">New Chapter</name> <name status="sa3b179" import="i466852">New Chapter</name>
</item> </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"/> <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>
<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"/> <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>
<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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Plot</name> <name status="sa3b179" import="i466852">Plot</name>
</item> </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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">Characters</name> <name status="sa3b179" import="i466852">Characters</name>
</item> </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"/> <meta expanded="False"/>
<name status="sa3b179" import="None">World</name> <name status="sa3b179" import="i466852">World</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
+1 -1
View File
@@ -66,7 +66,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
# Try to open a new (non-existent) file # Try to open a new (non-existent) file
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL) nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
assert nHandle is not None assert nHandle is not None
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle) xHandle = theProject.newFile("New File", nHandle)
theDoc = NWDoc(theProject, xHandle) theDoc = NWDoc(theProject, xHandle)
assert bool(theDoc) is True assert bool(theDoc) is True
assert repr(theDoc) == f"<NWDoc handle={xHandle}>" 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 assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject) theIndex = NWIndex(theProject)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
nItem = theProject.projTree[nHandle] nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle] cItem = theProject.projTree[cHandle]
@@ -260,8 +260,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
theIndex = NWIndex(theProject) theIndex = NWIndex(theProject)
# Some items for fail to scan tests # Some items for fail to scan tests
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c") dHandle = theProject.newFolder("Folder", "a508bb932959c")
xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c") xHandle = theProject.newFile("No Layout", "a508bb932959c")
xItem = theProject.projTree[xHandle] xItem = theProject.projTree[xHandle]
xItem.setLayout(nwItemLayout.NO_LAYOUT) xItem.setLayout(nwItemLayout.NO_LAYOUT)
@@ -278,20 +278,24 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
tHandle = theProject.trashFolder() tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle) 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 assert theIndex.scanText(xHandle, "Hello World!") is False
# Create the archive root # Create the archive root
aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE)
assert theProject.projTree[aHandle] is not None assert theProject.projTree[aHandle] is not None
xItem.setParent(aHandle) xItem.setParent(aHandle)
theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items # Make some usable items
tHandle = theProject.newFile("Title", nwItemClass.NOVEL, "a508bb932959c") tHandle = theProject.newFile("Title", "a508bb932959c")
pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c") pHandle = theProject.newFile("Page", "a508bb932959c")
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c") sHandle = theProject.newFile("Scene", "a508bb932959c")
# Text Indexing # Text Indexing
# ============= # =============
@@ -473,8 +477,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
assert theProject.openProject(nwMinimal) is True assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject) theIndex = NWIndex(theProject)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") nHandle = theProject.newFile("Hello", "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
assert theIndex.getNovelData("", "") is None assert theIndex.getNovelData("", "") is None
assert theIndex.getNovelData("a508bb932959c", "") is None assert theIndex.getNovelData("a508bb932959c", "") is None
@@ -628,9 +632,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# Novel Stats # Novel Stats
# =========== # ===========
hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c") hHandle = theProject.newFile("Chapter", "a508bb932959c")
sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c") sHandle = theProject.newFile("Scene One", "a508bb932959c")
tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c") tHandle = theProject.newFile("Scene Two", "a508bb932959c")
theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT
theProject.projTree[sHandle].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") theItem.setParent("0123456789abc")
assert theItem.itemParent == "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 # Order
theItem.setOrder(None) theItem.setOrder(None)
assert theItem.itemOrder == 0 assert theItem.itemOrder == 0
@@ -208,6 +220,7 @@ def testCoreItem_Methods(mockGUI):
# Status + Icon # Status + Icon
# ============= # =============
theItem.setType("FILE") theItem.setType("FILE")
theItem.setStatus("Note") theItem.setStatus("Note")
theItem.setImport("Minor") theItem.setImport("Minor")
@@ -217,11 +230,19 @@ def testCoreItem_Methods(mockGUI):
assert stT == "Note" assert stT == "Note"
assert isinstance(stI, QIcon) assert isinstance(stI, QIcon)
theItem.setImportStatus("Draft")
stT, stI = theItem.getImportStatus()
assert stT == "Draft"
theItem.setClass("CHARACTER") theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus() stT, stI = theItem.getImportStatus()
assert stT == "Minor" assert stT == "Minor"
assert isinstance(stI, QIcon) assert isinstance(stI, QIcon)
theItem.setImportStatus("Major")
stT, stI = theItem.getImportStatus()
assert stT == "Major"
# Representation # Representation
# ============== # ==============
@@ -263,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI):
assert theItem.itemType == nwItemType.FILE assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH") theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH assert theItem.itemType == nwItemType.TRASH
# Alternative
theItem.setType(nwItemType.ROOT) theItem.setType(nwItemType.ROOT)
assert theItem.itemType == nwItemType.ROOT assert theItem.itemType == nwItemType.ROOT
@@ -282,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI):
assert theItem.itemClass == nwItemClass.NO_CLASS assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClass("NONSENSE") theItem.setClass("NONSENSE")
assert theItem.itemClass == nwItemClass.NO_CLASS assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClass("NO_CLASS") theItem.setClass("NO_CLASS")
assert theItem.itemClass == nwItemClass.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") theItem.setClass("NOVEL")
assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is False
theItem.setClass("PLOT") theItem.setClass("PLOT")
assert theItem.itemClass == nwItemClass.PLOT assert theItem.itemClass == nwItemClass.PLOT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CHARACTER") theItem.setClass("CHARACTER")
assert theItem.itemClass == nwItemClass.CHARACTER assert theItem.itemClass == nwItemClass.CHARACTER
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("WORLD") theItem.setClass("WORLD")
assert theItem.itemClass == nwItemClass.WORLD assert theItem.itemClass == nwItemClass.WORLD
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("TIMELINE") theItem.setClass("TIMELINE")
assert theItem.itemClass == nwItemClass.TIMELINE assert theItem.itemClass == nwItemClass.TIMELINE
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("OBJECT") theItem.setClass("OBJECT")
assert theItem.itemClass == nwItemClass.OBJECT assert theItem.itemClass == nwItemClass.OBJECT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ENTITY") theItem.setClass("ENTITY")
assert theItem.itemClass == nwItemClass.ENTITY assert theItem.itemClass == nwItemClass.ENTITY
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CUSTOM") theItem.setClass("CUSTOM")
assert theItem.itemClass == nwItemClass.CUSTOM assert theItem.itemClass == nwItemClass.CUSTOM
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ARCHIVE") theItem.setClass("ARCHIVE")
assert theItem.itemClass == nwItemClass.ARCHIVE assert theItem.itemClass == nwItemClass.ARCHIVE
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is True
theItem.setClass("TRASH") theItem.setClass("TRASH")
assert theItem.itemClass == nwItemClass.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) theItem.setClass(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL assert theItem.itemClass == nwItemClass.NOVEL
@@ -332,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI):
theItem.setLayout("NOTE") theItem.setLayout("NOTE")
assert theItem.itemLayout == nwItemLayout.NOTE assert theItem.itemLayout == nwItemLayout.NOTE
# Alternatives # Alternative
theItem.setLayout(nwItemLayout.NOTE) theItem.setLayout(nwItemLayout.NOTE)
assert theItem.itemLayout == nwItemLayout.NOTE assert theItem.itemLayout == nwItemLayout.NOTE
# END Test testCoreItem_LayoutSetter # 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 @pytest.mark.core
def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
"""Test packing and unpacking XML objects for the NWItem class. """Test packing and unpacking XML objects for the NWItem class.
@@ -353,6 +478,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject) theItem = NWItem(theProject)
theItem.setHandle("0123456789abc") theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc") theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1) theItem.setOrder(1)
theItem.setName("A Name") theItem.setName("A Name")
theItem.setClass("NOVEL") theItem.setClass("NOVEL")
@@ -370,9 +496,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent) theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b'<content>' b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" order="1" type="FILE" class="NOVEL" ' b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'layout="NOTE"><meta charCount="7" wordCount="5" paraCount="3" cursorPos="11"/>' b'type="FILE" class="NOVEL" layout="NOTE"><meta charCount="7" wordCount="5" paraCount="3" '
b'<name status="None" import="%s" exported="False">A Name</name></item>' b'cursorPos="11"/><name status="None" import="%s" exported="False">A Name</name></item>'
b'</content>' b'</content>'
) % bytes(constData.importKeys[3], encoding="utf8") ) % bytes(constData.importKeys[3], encoding="utf8")
@@ -381,6 +507,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0]) assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc" assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc" assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1 assert theItem.itemOrder == 1
assert theItem.isExported is False assert theItem.isExported is False
assert theItem.paraCount == 3 assert theItem.paraCount == 3
@@ -399,6 +526,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject) theItem = NWItem(theProject)
theItem.setHandle("0123456789abc") theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc") theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1) theItem.setOrder(1)
theItem.setName("A Name") theItem.setName("A Name")
theItem.setClass("NOVEL") theItem.setClass("NOVEL")
@@ -417,9 +545,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent) theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b'<content>' b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" order="1" type="FOLDER" ' b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'class="NOVEL"><meta expanded="True"/><name status="%s" import="None">A Name</name>' b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="%s" '
b'</item>' b'import="None">A Name</name></item>'
b'</content>' b'</content>'
) % bytes(constData.statusKeys[1], encoding="utf8") ) % bytes(constData.statusKeys[1], encoding="utf8")
@@ -428,6 +556,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0]) assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc" assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc" assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1 assert theItem.itemOrder == 1
assert theItem.isExpanded is True assert theItem.isExpanded is True
assert theItem.isExported 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.closeProject() is True
assert theProject.openProject(projFile) is True assert theProject.openProject(projFile) is True
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str)
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None)) assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str)
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None)) assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str)
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None)) assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str)
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), 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.closeProject() is True
assert theProject.openProject(projFile) is True assert theProject.openProject(projFile) is True
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str) assert isinstance(theProject.newFile("Hello", "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str) assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str)
assert theProject.projChanged assert theProject.projChanged
assert theProject.saveProject() is True assert theProject.saveProject() is True
assert theProject.closeProject() is True assert theProject.closeProject() is True
@@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
theProject.projTree._treeOrder.append("01234567789abc") theProject.projTree._treeOrder.append("01234567789abc")
# Add an item with a non-existent parent # 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") theProject.projTree[nHandle].setParent("cba9876543210")
assert theProject.projTree[nHandle].itemParent == "cba9876543210" assert theProject.projTree[nHandle].itemParent == "cba9876543210"
@@ -740,7 +740,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData):
# Change Importance # Change Importance
# ================= # =================
fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568") fHandle = theProject.newFile("Jane Doe", "73475cb40a568")
theProject.projTree[fHandle].setImport("Main") theProject.projTree[fHandle].setImport("Main")
assert theProject.projTree[fHandle].itemImport == constData.importKeys[3] assert theProject.projTree[fHandle].itemImport == constData.importKeys[3]
@@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum) assert theProject.openProject(nwLipsum) is True
assert theProject.projTree["636b6aa9b697b"] is None 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 # First Item with Meta Data
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") 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 os
import pytest import pytest
import random
from lxml import etree from lxml import etree
from hashlib import sha256 from hashlib import sha256
@@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles
def mockItems(mockGUI): def mockItems(mockGUI):
"""Create a list of mock items. """Create a list of mock items.
""" """
random.seed(42)
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
itemA = NWItem(theProject) itemA = NWItem(theProject)
@@ -103,7 +105,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE), ("a000000000002", None, itemE),
("a000000000003", None, itemF), ("a000000000003", None, itemF),
("a000000000004", None, itemG), ("a000000000004", None, itemG),
("b000000000002", "a000000000002", itemH), ("b000000000002", "a000000000004", itemH),
] ]
return theItems return theItems
@@ -120,22 +122,21 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree._handleSeed == 42 assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__) # Check that tree is empty (calls NWTree.__bool__)
assert not theTree assert bool(theTree) is False
# Check for archive and trash folders # Check for archive and trash folders
assert theTree.trashRoot() is None assert theTree.trashRoot() is None
assert theTree.archiveRoot() is None
assert not theTree.isTrashRoot("a000000000003")
aHandles = [] aHandles = []
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle) 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__) # Check that tree is not empty (calls __bool__)
assert theTree assert bool(theTree) is True
# Check the number of elements (calls __len__) # Check the number of elements (calls __len__)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
@@ -149,8 +150,29 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# Check that we have the correct archive and trash folders # Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003" assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002" assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrashRoot("a000000000003") 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 # Try to add another trash folder
itemT = NWItem(theProject) itemT = NWItem(theProject)
@@ -159,7 +181,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.TRASH itemT._class = nwItemClass.TRASH
itemT._expanded = False itemT._expanded = False
assert not theTree.append("1234567890abc", None, itemT) assert theTree.append("1234567890abc", None, itemT) is False
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
# Generate handle automatically # Generate handle automatically
@@ -169,14 +191,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.NOVEL itemT._class = nwItemClass.NOVEL
itemT._layout = nwItemLayout.DOCUMENT 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 assert len(theTree) == len(mockItems) + 1
theList = theTree.handles() theList = theTree.handles()
assert theList[-1] == "73475cb40a568" assert theList[-1] == "73475cb40a568"
# Try to add existing handle # 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 assert len(theTree) == len(mockItems) + 1
# Delete a non-existing item # Delete a non-existing item
@@ -196,7 +219,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000002"] del theTree["a000000000002"]
assert len(theTree) == len(mockItems) - 2 assert len(theTree) == len(mockItems) - 2
assert "a000000000002" not in theTree assert "a000000000002" not in theTree
assert theTree.archiveRoot() is None
del theTree["a000000000003"] del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3 assert len(theTree) == len(mockItems) - 3
@@ -215,31 +237,43 @@ def testCoreTree_Methods(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) 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 # Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("blabla", nwItemType.FILE) is False
assert theTree.checkType("b000000000001", nwItemType.FILE) is False assert theTree.checkType("b000000000001", nwItemType.FILE) is False
assert theTree.checkType("c000000000001", nwItemType.FILE) is True assert theTree.checkType("c000000000001", nwItemType.FILE) is True
# Root item lookup # Root item lookup
theTree._treeRoots.append("stuff")
assert theTree.findRoot(nwItemClass.WORLD) is None assert theTree.findRoot(nwItemClass.WORLD) is None
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
# Check for root uniqueness # Add a fake item to root and check that it can handle it
assert theTree.checkRootUnique(nwItemClass.CUSTOM) theTree._treeRoots["0000000000000"] = NWItem(theProject)
assert theTree.checkRootUnique(nwItemClass.WORLD) assert theTree.findRoot(nwItemClass.WORLD) is None
assert not theTree.checkRootUnique(nwItemClass.NOVEL) del theTree._treeRoots["0000000000000"]
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
# Get item path # Get item path
assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("stuff") == []
@@ -247,6 +281,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001" "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 # Break the folder parent handle
theTree["b000000000001"]._parent = "stuff" theTree["b000000000001"]._parent = "stuff"
assert theTree.getItemPath("c000000000001") == [ assert theTree.getItemPath("c000000000001") == [
@@ -371,7 +412,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_XMLPackUnpack(mockGUI, mockItems): def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData):
"""Test packing and unpacking the tree to and from XML. """Test packing and unpacking the tree to and from XML.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -379,37 +420,47 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
nwXML = etree.Element("novelWriterXML") nwXML = etree.Element("novelWriterXML")
theTree.packXML(nwXML) theTree.packXML(nwXML)
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes((
b'<novelWriterXML>' '<novelWriterXML>'
b'<content count="8">' '<content count="8">'
b'<item handle="a000000000001" parent="None" order="0" type="ROOT" class="NOVEL"><meta ' '<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
b'expanded="True"/><name status="None" import="None">Novel</name></item>' 'class="NOVEL"><meta expanded="True"/><name status="{s0}" '
b'<item handle="b000000000001" parent="a000000000001" order="0" type="FOLDER" ' 'import="{i0}">Novel</name></item>'
b'class="NOVEL"><meta expanded="True"/><name status="None" import="None">Act One</name>' '<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
b'</item>' 'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="{s0}" '
b'<item handle="c000000000001" parent="b000000000001" order="0" type="FILE" class="NOVEL" ' 'import="{i0}">Act One</name></item>'
b'layout="DOCUMENT"><meta charCount="300" wordCount="50" paraCount="2" cursorPos="0"/>' '<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
b'<name status="None" import="None" exported="True">Chapter One</name></item>' 'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" '
b'<item handle="c000000000002" parent="b000000000001" order="0" type="FILE" class="NOVEL" ' 'paraCount="2" cursorPos="0"/><name status="{s0}" import="{i0}" '
b'layout="DOCUMENT"><meta charCount="3000" wordCount="500" paraCount="20" cursorPos="0"/>' 'exported="True">Chapter One</name></item>'
b'<name status="None" import="None" exported="True">Scene One</name></item>' '<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
b'<item handle="a000000000002" parent="None" order="0" type="ROOT" class="ARCHIVE"><meta ' 'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="3000" wordCount="500" '
b'expanded="False"/><name status="None" import="None">Outtakes</name></item>' 'paraCount="20" cursorPos="0"/><name status="{s0}" import="{i0}" '
b'<item handle="a000000000003" parent="None" order="0" type="TRASH" class="TRASH"><meta ' 'exported="True">Scene One</name></item>'
b'expanded="False"/><name status="None" import="None">Trash</name></item>' '<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
b'<item handle="a000000000004" parent="None" order="0" type="ROOT" class="CHARACTER">' 'class="ARCHIVE"><meta expanded="False"/><name status="{s0}" '
b'<meta expanded="True"/><name status="None" import="None">Characters</name></item>' 'import="{i0}">Outtakes</name></item>'
b'<item handle="b000000000002" parent="a000000000002" order="0" type="FILE" ' '<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="TRASH" '
b'class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" paraCount="16" ' 'class="TRASH"><meta expanded="False"/><name status="{s0}" '
b'cursorPos="0"/><name status="None" import="None" exported="True">Jane Doe</name></item>' 'import="{i0}">Trash</name></item>'
b'</content>' '<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
b'</novelWriterXML>' '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() theTree.clear()
assert len(theTree) == 0 assert len(theTree) == 0
@@ -429,6 +480,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff") theTree._treeOrder.append("stuff")
+3 -3
View File
@@ -58,9 +58,9 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.switchFocus(nwWidget.TREE) nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection() nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True assert nwGUI.saveProject() is True
assert nwGUI.closeProject() 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.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection() nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True assert nwGUI.saveProject() is True
assert nwGUI.closeProject() 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) mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwSplit._doSplit() is False 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 # Clear the list
nwSplit.listBox.clear() nwSplit.listBox.clear()
assert nwSplit._doSplit() is False assert nwSplit._doSplit() is False
+1 -1
View File
@@ -175,7 +175,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData):
itemEdit.show() itemEdit.show()
# Check Existing Settings # Check Existing Settings
assert itemEdit.editName.text() == "New File" assert itemEdit.editName.text() == "New Note"
assert itemEdit.editStatus.currentData() == constData.importKeys[0] assert itemEdit.editStatus.currentData() == constData.importKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True 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.gui.doceditor import GuiDocEditor
from novelwriter.core import countWords 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 from novelwriter.constants import nwKeyWords, nwUnicode
keyDelay = 2 keyDelay = 2
@@ -1143,7 +1143,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
# Create Character # Create Character
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" 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.openDocument(cHandle) is True
assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.saveDocument() 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 pytest
import os import os
from tools import writeFile
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -33,207 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass
@pytest.mark.gui @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. """Test adding and removing items from the project tree.
""" """
# Block message box # 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, "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(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.treeView nwTree = nwGUI.treeView
## # Try to add item with no project
# Add New Items assert nwTree.newTreeItem(nwItemType.FILE) is False
##
# Try to add and move item with no project # Create a project
assert not nwTree.newTreeItem(nwItemType.FILE, None) nwGUI.theProject.projTree.setSeed(42)
assert not nwTree.moveTreeItem(1) prjDir = os.path.join(fncDir, "project")
assert nwGUI.newProject({"projPath": prjDir}) is True
# Open a project # No itemType set
assert nwGUI.openProject(nwMinimal) 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 # No location selected for new item
nwTree.clearSelection() nwTree.clearSelection()
assert not nwTree.newTreeItem(nwItemType.FILE, None) caplog.clear()
assert not nwTree.newTreeItem(nwItemType.FOLDER, None) assert nwTree.newTreeItem(nwItemType.FILE) is False
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) assert nwTree.newTreeItem(nwItemType.FOLDER) is False
assert "Did not find anywhere" in caplog.text
# No itemType set or ROOT, but no class # Create new folder as child of Novel folder
assert not nwTree.newTreeItem(None, None) nwTree.setSelectedHandle("73475cb40a568")
assert not nwTree.newTreeItem(nwItemType.ROOT, None) 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 # Add a new file in the new folder
chItem = nwTree._getTreeItem("a6d311a93600a") nwTree.setSelectedHandle("031b4af5197ec")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) assert nwTree.newTreeItem(nwItemType.FILE) is True
chItem.setExpanded(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) # Add a new file next to the other new file
assert nwTree.newTreeItem(nwItemType.FILE, None) nwTree.setSelectedHandle("41cfc0d1f2d12")
assert nwTree.newTreeItem(nwItemType.FOLDER, None) 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 # Add a new file to the characters folder
assert nwTree.getTreeFromHandle("a6d311a93600a") == [ nwTree.setSelectedHandle("71ee45a3c0db9")
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "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 # Make sure the sibling folder bug trap works
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate nwTree.setSelectedHandle("2858dcd1057d3")
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid 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 # Get the trash folder
monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2) nwTree._addTrashRoot()
chItem = nwTree._getTreeItem("71ee45a3c0db9") trashHandle = nwGUI.theProject.trashFolder()
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) nwTree.setSelectedHandle(trashHandle)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None) assert nwTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text
## # Other Checks
# Move Items # ============
##
nwTree.setSelectedHandle("8c659a11cd429") # Also check error handling in reveal function
assert nwTree.revealNewTreeItem("abc") is False
# Shift focus and try to move item # Add an item that cannot be displayed in the tree
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) nHandle = nwGUI.theProject.newFile("Test", None)
assert not nwTree.moveTreeItem(1) assert nwTree.revealNewTreeItem(nHandle) is False
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)
# Clean up
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
nwGUI.closeProject() 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 PyQt5.QtWidgets import QMessageBox
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwItemClass, nwState from novelwriter.enum import nwState
@pytest.mark.gui @pytest.mark.gui
@@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.theProject.projTree.setSeed(42) nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True 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 = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n") newDoc.writeDocument("# A Note\n\n")
nwGUI.treeView.revealNewTreeItem(cHandle) nwGUI.treeView.revealNewTreeItem(cHandle)