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