diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index e06aba68..bcaef71a 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -42,6 +42,7 @@ doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg
+down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
@@ -74,3 +75,4 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
+up = typ_chevron-up.svg
diff --git a/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg b/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg
new file mode 100644
index 00000000..53389084
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg b/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg
new file mode 100644
index 00000000..9ac7e927
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 56e18b2a..8639908f 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -42,6 +42,7 @@ doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg
+down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
@@ -74,3 +75,4 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
+up = typ_chevron-up.svg
diff --git a/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg b/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg
new file mode 100644
index 00000000..6ba80643
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/typ_chevron-down.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg b/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg
new file mode 100644
index 00000000..1b9eb901
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/typ_chevron-up.svg
@@ -0,0 +1,31 @@
+
+
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 8e17a7ee..69f6bc05 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -184,6 +184,12 @@ def checkIntRange(value, first, last, default):
return default
+def minmax(value, minVal, maxVal):
+ """Make sure an integer is between min and max value (inclusive).
+ """
+ return min(maxVal, max(minVal, value))
+
+
def checkIntTuple(value, valid, default):
"""Check that an int is an element of a tuple. If it isn't, return
the default value.
@@ -245,6 +251,13 @@ def formatTime(tS):
# String Functions
# =============================================================================================== #
+def simplified(string):
+ """Take a string an strip leading and trailing whitespaces, and
+ replace all occurences of (multiple) whitespaces with a 0x20 space.
+ """
+ return " ".join(str(string).strip().split())
+
+
def splitVersionNumber(value):
"""Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index b7332478..45ae9acc 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -679,16 +679,18 @@ class NWIndex():
logException()
self._indexBroken = True
- # Check that project files are indexed
- for fHandle in self.theProject.projFiles:
- if fHandle not in self._fileMeta:
- self._indexBroken = True
- break
-
- logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
-
if self._indexBroken:
self.clearIndex()
+ logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
+ return
+
+ # If the index was ok, we check that project files are indexed
+ for fHandle in self.theProject.projFiles:
+ if fHandle not in self._fileMeta:
+ logger.warning("Item '%s' is not in the index", fHandle)
+ self.reIndexHandle(fHandle)
+
+ logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
return
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 8b04a0d3..1edd93b8 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -29,7 +29,7 @@ from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
- checkInt, isHandle, isItemClass, isItemLayout, isItemType
+ checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, nwLists, trConst
@@ -287,11 +287,11 @@ class NWItem():
the current item based on its class.
"""
if self._class in nwLists.CLS_NOVEL:
- stName = self.theProject.statusItems.checkEntry(self._status)
- stIcon = self.theProject.statusItems.getIcon(stName)
+ stName = self.theProject.statusItems.name(self._status)
+ stIcon = self.theProject.statusItems.icon(self._status)
else:
- stName = self.theProject.importItems.checkEntry(self._import)
- stIcon = self.theProject.importItems.getIcon(stName)
+ stName = self.theProject.importItems.name(self._import)
+ stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
def setImportStatus(self, theLabel):
@@ -308,153 +308,153 @@ class NWItem():
# Set Item Values
##
- def setName(self, theName):
+ def setName(self, name):
"""Set the item name.
"""
- if isinstance(theName, str):
- self._name = theName.strip()
+ if isinstance(name, str):
+ self._name = simplified(name)
else:
self._name = ""
return
- def setHandle(self, theHandle):
+ def setHandle(self, tHandle):
"""Set the item handle, and ensure it is valid.
"""
- if isHandle(theHandle):
- self._handle = theHandle
+ if isHandle(tHandle):
+ self._handle = tHandle
else:
self._handle = None
return
- def setParent(self, theParent):
+ def setParent(self, pHandle):
"""Set the parent handle, and ensure it is valid.
"""
- if theParent is None:
+ if pHandle is None:
self._parent = None
- elif isHandle(theParent):
- self._parent = theParent
+ elif isHandle(pHandle):
+ self._parent = pHandle
else:
self._parent = None
return
- def setRoot(self, theRoot):
+ def setRoot(self, rHandle):
"""Set the root handle, and ensure it is valid.
"""
- if theRoot is None:
+ if rHandle is None:
self._root = None
- elif isHandle(theRoot):
- self._root = theRoot
+ elif isHandle(rHandle):
+ self._root = rHandle
else:
self._root = None
return
- def setOrder(self, theOrder):
+ 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
the moment.
"""
- self._order = checkInt(theOrder, 0)
+ self._order = checkInt(order, 0)
return
- def setType(self, theType):
+ def setType(self, itemType):
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
- if isinstance(theType, nwItemType):
- self._type = theType
- elif isItemType(theType):
- self._type = nwItemType[theType]
+ if isinstance(itemType, nwItemType):
+ self._type = itemType
+ elif isItemType(itemType):
+ self._type = nwItemType[itemType]
else:
- logger.error("Unrecognised item type '%s'", theType)
+ logger.error("Unrecognised item type '%s'", itemType)
self._type = nwItemType.NO_TYPE
return
- def setClass(self, theClass):
+ def setClass(self, itemClass):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
- if isinstance(theClass, nwItemClass):
- self._class = theClass
- elif isItemClass(theClass):
- self._class = nwItemClass[theClass]
+ if isinstance(itemClass, nwItemClass):
+ self._class = itemClass
+ elif isItemClass(itemClass):
+ self._class = nwItemClass[itemClass]
else:
- logger.error("Unrecognised item class '%s'", theClass)
+ logger.error("Unrecognised item class '%s'", itemClass)
self._class = nwItemClass.NO_CLASS
return
- def setLayout(self, theLayout):
+ def setLayout(self, itemLayout):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
- if isinstance(theLayout, nwItemLayout):
- self._layout = theLayout
- elif isItemLayout(theLayout):
- self._layout = nwItemLayout[theLayout]
- elif theLayout in nwLists.DEP_LAYOUT:
+ if isinstance(itemLayout, nwItemLayout):
+ self._layout = itemLayout
+ elif isItemLayout(itemLayout):
+ self._layout = nwItemLayout[itemLayout]
+ elif itemLayout in nwLists.DEP_LAYOUT:
self._layout = nwItemLayout.DOCUMENT
else:
- logger.error("Unrecognised item layout '%s'", theLayout)
+ logger.error("Unrecognised item layout '%s'", itemLayout)
self._layout = nwItemLayout.NO_LAYOUT
return
- def setStatus(self, theStatus):
+ def setStatus(self, itemStatus):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
- self._status = self.theProject.statusItems.checkEntry(theStatus)
+ self._status = self.theProject.statusItems.check(itemStatus)
return
- def setImport(self, theImport):
+ def setImport(self, itemImport):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
- self._import = self.theProject.importItems.checkEntry(theImport)
+ self._import = self.theProject.importItems.check(itemImport)
return
- def setExpanded(self, expState):
+ def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
- if isinstance(expState, str):
- self._expanded = (expState == str(True))
+ if isinstance(state, str):
+ self._expanded = (state == str(True))
else:
- self._expanded = (expState is True)
+ self._expanded = (state is True)
return
- def setExported(self, expState):
+ def setExported(self, state):
"""Set the export flag.
"""
- if isinstance(expState, str):
- self._exported = (expState == str(True))
+ if isinstance(state, str):
+ self._exported = (state == str(True))
else:
- self._exported = (expState is True)
+ self._exported = (state is True)
return
##
# Set Document Meta Data
##
- def setCharCount(self, theCount):
+ def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
- self._charCount = max(0, checkInt(theCount, 0))
+ self._charCount = max(0, checkInt(count, 0))
return
- def setWordCount(self, theCount):
+ def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer.
"""
- self._wordCount = max(0, checkInt(theCount, 0))
+ self._wordCount = max(0, checkInt(count, 0))
return
- def setParaCount(self, theCount):
+ def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer.
"""
- self._paraCount = max(0, checkInt(theCount, 0))
+ self._paraCount = max(0, checkInt(count, 0))
return
- def setCursorPos(self, thePosition):
+ def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer.
"""
- self._cursorPos = max(0, checkInt(thePosition, 0))
+ self._cursorPos = max(0, checkInt(position, 0))
return
def saveInitialCount(self):
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 3e10dcd7..0d71b81f 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -44,7 +44,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException
from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
- makeFileNameSafe, hexToInt
+ makeFileNameSafe, hexToInt, simplified
)
from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels
@@ -217,16 +217,16 @@ class NWProject():
}
self.spellCheck = False
self.autoOutline = True
- self.statusItems = NWStatus()
- self.statusItems.addEntry(self.tr("New"), (100, 100, 100))
- self.statusItems.addEntry(self.tr("Note"), (200, 50, 0))
- self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0))
- self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0))
- self.importItems = NWStatus()
- self.importItems.addEntry(self.tr("New"), (100, 100, 100))
- self.importItems.addEntry(self.tr("Minor"), (200, 50, 0))
- self.importItems.addEntry(self.tr("Major"), (200, 150, 0))
- self.importItems.addEntry(self.tr("Main"), (50, 200, 0))
+ self.statusItems = NWStatus(NWStatus.STATUS)
+ self.statusItems.write(None, self.tr("New"), (100, 100, 100))
+ self.statusItems.write(None, self.tr("Note"), (200, 50, 0))
+ self.statusItems.write(None, self.tr("Draft"), (200, 150, 0))
+ self.statusItems.write(None, self.tr("Finished"), (50, 200, 0))
+ self.importItems = NWStatus(NWStatus.IMPORT)
+ self.importItems.write(None, self.tr("New"), (100, 100, 100))
+ self.importItems.write(None, self.tr("Minor"), (200, 50, 0))
+ self.importItems.write(None, self.tr("Major"), (200, 150, 0))
+ self.importItems.write(None, self.tr("Main"), (50, 200, 0))
self.lastEdited = None
self.lastViewed = None
self.lastWCount = 0
@@ -266,6 +266,7 @@ class NWProject():
logger.error("No project path set for the new project")
return False
+ self.clearProject()
if not self.setProjectPath(projPath, newProject=True):
return False
@@ -532,14 +533,16 @@ class NWProject():
if xItem.text is None:
continue
if xItem.tag == "name":
- logger.verbose("Working Title: '%s'", xItem.text)
- self.projName = xItem.text
+ self.projName = checkString(simplified(xItem.text), "")
+ logger.verbose("Working Title: '%s'", self.projName)
elif xItem.tag == "title":
- logger.verbose("Title is '%s'", xItem.text)
- self.bookTitle = xItem.text
+ self.bookTitle = checkString(simplified(xItem.text), "")
+ logger.verbose("Title is '%s'", self.bookTitle)
elif xItem.tag == "author":
- logger.verbose("Author: '%s'", xItem.text)
- self.bookAuthors.append(xItem.text)
+ author = checkString(simplified(xItem.text), "")
+ if author:
+ self.bookAuthors.append(author)
+ logger.verbose("Author: '%s'", author)
elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount":
@@ -693,6 +696,8 @@ class NWProject():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
+ # Save Status/Importance
+ self.countStatus()
xStatus = etree.SubElement(xSettings, "status")
self.statusItems.packXML(xStatus)
xStatus = etree.SubElement(xSettings, "importance")
@@ -959,14 +964,14 @@ class NWProject():
"""Set the project name (working title), This is the the title
used for backup files etc.
"""
- self.projName = projName.strip()
+ self.projName = simplified(projName)
self.setProjectChanged(True)
return True
def setBookTitle(self, bookTitle):
"""Set the book title, that is, the title to include in exports.
"""
- self.bookTitle = bookTitle.strip()
+ self.bookTitle = simplified(bookTitle)
self.setProjectChanged(True)
return True
@@ -978,7 +983,7 @@ class NWProject():
self.bookAuthors = []
for bookAuthor in bookAuthors.splitlines():
- bookAuthor = bookAuthor.strip()
+ bookAuthor = simplified(bookAuthor)
if bookAuthor == "":
continue
self.bookAuthors.append(bookAuthor)
@@ -1024,7 +1029,8 @@ class NWProject():
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
- return True
+ return True
+ return False
def setProjectLang(self, theLang):
"""Set the project-specific language.
@@ -1071,34 +1077,22 @@ class NWProject():
self.setProjectChanged(True)
return True
- def setStatusColours(self, newCols):
- """Update the list of novel file status flags. Also iterate
- through the project and replace keys that have been renamed.
+ def setStatusColours(self, newCols, delCols):
+ """Update the list of novel file status flags.
"""
- replaceMap = self.statusItems.setNewEntries(newCols)
- for nwItem in self.projTree:
- if nwItem.itemClass in nwLists.CLS_NOVEL:
- if nwItem.itemStatus in replaceMap:
- nwItem.setStatus(replaceMap[nwItem.itemStatus])
- self.setProjectChanged(True)
- return True
+ return self._setStatusImport(newCols, delCols, self.statusItems)
- def setImportColours(self, newCols):
- """Update the list of note file importance flags. Also iterate
- through the project and replace keys that have been renamed.
+ def setImportColours(self, newCols, delCols):
+ """Update the list of note file importance flags.
"""
- replaceMap = self.importItems.setNewEntries(newCols)
- for nwItem in self.projTree:
- if nwItem.itemClass not in nwLists.CLS_NOVEL:
- if nwItem.itemImport in replaceMap:
- nwItem.setImport(replaceMap[nwItem.itemImport])
- self.setProjectChanged(True)
- return True
+ return self._setStatusImport(newCols, delCols, self.importItems)
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary.
"""
- self.autoReplace = autoReplace
+ self.autoReplace = {}
+ for key, entry in autoReplace.items():
+ self.autoReplace[key] = simplified(entry)
self.setProjectChanged(True)
return True
@@ -1107,7 +1101,9 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
- self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
+ self.titleFormat[valKey] = checkString(
+ simplified(valEntry), self.titleFormat[valKey]
+ )
return True
def setProjectChanged(self, bValue):
@@ -1213,9 +1209,9 @@ class NWProject():
self.importItems.resetCounts()
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
- self.statusItems.countEntry(nwItem.itemStatus)
+ self.statusItems.increment(nwItem.itemStatus)
else:
- self.importItems.countEntry(nwItem.itemImport)
+ self.importItems.increment(nwItem.itemImport)
return
def localLookup(self, theWord):
@@ -1229,6 +1225,28 @@ class NWProject():
# Internal Functions
##
+ def _setStatusImport(self, new, delete, target):
+ """Update the list of novel file status or importance flags, and
+ delete those that have been requested deleted.
+ """
+ if not (new or delete):
+ return False
+
+ order = []
+ for entry in new:
+ key = entry.get("key", None)
+ name = entry.get("name", "")
+ cols = entry.get("cols", (100, 100, 100))
+ if name:
+ order.append(target.write(key, name, cols))
+
+ for key in delete:
+ target.remove(key)
+
+ target.reorder(order)
+
+ return True
+
def _loadProjectLocalisation(self):
"""Load the language data for the current project language.
"""
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index 2ca8ed53..4bade5e7 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -4,7 +4,8 @@ novelWriter – Project Item Status Class
Data class for the status/importance settings of a project item
File History:
-Created: 2019-05-19 [0.1.3]
+Created: 2019-05-19 [0.1.3]
+Rewritten: 2022-04-05 [1.7a0]
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -23,6 +24,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import random
import logging
import novelwriter
@@ -30,134 +32,208 @@ from lxml import etree
from PyQt5.QtGui import QIcon, QPixmap, QColor
-from novelwriter.common import checkInt
+from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
- def __init__(self):
+ STATUS = 1
+ IMPORT = 2
+
+ def __init__(self, type):
+
+ self._type = type
+ self._store = {}
+ self._reverse = {}
+ self._default = None
- self._theLabels = []
- self._theColours = []
- self._theCounts = []
- self._theIcons = []
- self._theMap = {}
- self._theLength = 0
- self._theIndex = 0
self._iconSize = novelwriter.CONFIG.pxInt(32)
+ pixmap = QPixmap(self._iconSize, self._iconSize)
+ pixmap.fill(QColor(100, 100, 100))
+ self._defaultIcon = QIcon(pixmap)
+
+ if self._type == self.STATUS:
+ self._prefix = "s"
+ elif self._type == self.IMPORT:
+ self._prefix = "i"
+ else:
+ raise Exception("This is a bug!")
return
- def addEntry(self, theLabel, theColours):
- """Add a status entry to the status object, but ensure it isn't
- a duplicate.
+ def write(self, key, name, cols, count=None):
+ """Add or update a status entry. If the key is invalid, a new
+ key is generated.
"""
- theLabel = theLabel.strip()
- if self._getIndex(theLabel) is None:
- theIcon = QPixmap(self._iconSize, self._iconSize)
- theIcon.fill(QColor(*theColours))
- self._theIcons.append(QIcon(theIcon))
- self._theLabels.append(theLabel)
- self._theColours.append(theColours)
- self._theCounts.append(0)
- self._theMap[theLabel] = self._theLength
- self._theLength += 1
+ if not self._isKey(key):
+ key = self._newKey()
+ if not isinstance(cols, tuple):
+ cols = (100, 100, 100)
+ if len(cols) != 3:
+ cols = (100, 100, 100)
+
+ pixmap = QPixmap(self._iconSize, self._iconSize)
+ pixmap.fill(QColor(*cols))
+
+ name = simplified(name)
+ if count is None:
+ count = self._store[key]["count"] if key in self._store else 0
+
+ self._store[key] = {
+ "name": name,
+ "icon": QIcon(pixmap),
+ "cols": cols,
+ "count": count,
+ }
+ self._reverse[name] = key
+
+ if self._default is None:
+ self._default = key
+
+ return key
+
+ def remove(self, key):
+ """Remove an entry in the list, but not if the count is larger
+ than 0.
+ """
+ if key not in self._store:
+ return False
+ if self._store[key]["count"] > 0:
+ return False
+
+ del self._reverse[self._store[key]["name"]]
+ del self._store[key]
+
+ keys = list(self._store.keys())
+ if key == self._default:
+ if len(keys) > 0:
+ self._default = keys[0]
+ else:
+ self._default = None
return True
- def checkEntry(self, theStatus):
- """Check if a status value is valid, and returns the safe
- reference to be used internally.
+ def check(self, value):
+ """Check the key against the stored status names.
"""
- if isinstance(theStatus, str):
- if self._getIndex(theStatus) is not None:
- return theStatus.strip()
- return self._theLabels[0]
+ if self._isKey(value) and value in self._store:
+ return value
+ elif value in self._reverse:
+ return self._reverse[value]
+ elif self._default is not None:
+ return self._default
+ else:
+ return ""
- def getIcon(self, theLabel):
- """Return the icon for the given status item.
+ def name(self, key):
+ """Return the name associated with a given key.
"""
- theIndex = self._getIndex(theLabel)
- if theIndex is not None:
- return self._theIcons[theIndex]
- return QIcon()
+ if key in self._store:
+ return self._store[key]["name"]
+ elif self._default is not None:
+ return self._store[self._default]["name"]
+ else:
+ return ""
- def setNewEntries(self, newList):
- """Update the list of entries after they have been modified by
- the GUI tool.
+ def cols(self, key):
+ """Return the colours associated with a given key.
"""
- replaceMap = {}
+ if key in self._store:
+ return self._store[key]["cols"]
+ elif self._default is not None:
+ return self._store[self._default]["cols"]
+ else:
+ return (100, 100, 100)
- if newList is not None:
- self._theLabels = []
- self._theColours = []
- self._theCounts = []
- self._theIcons = []
- self._theMap = {}
- self._theLength = 0
- self._theIndex = 0
+ def count(self, key):
+ """Return the count associated with a given key.
+ """
+ if key in self._store:
+ return self._store[key]["count"]
+ elif self._default is not None:
+ return self._store[self._default]["count"]
+ else:
+ return 0
- for nName, nR, nG, nB, oName in newList:
- self.addEntry(nName, (nR, nG, nB))
- if nName != oName and oName is not None:
- replaceMap[oName] = nName
+ def icon(self, key):
+ """Return the icon associated with a given key.
+ """
+ if key in self._store:
+ return self._store[key]["icon"]
+ elif self._default is not None:
+ return self._store[self._default]["icon"]
+ else:
+ return self._defaultIcon
- return replaceMap
+ def reorder(self, order):
+ """Reorder the items according to list.
+ """
+ if len(order) != len(self._store):
+ logger.error("Length mismatch between new and old order")
+ return False
+
+ if order == list(self._store.keys()):
+ return False
+
+ store = {}
+ for key in order:
+ if key in self._store:
+ store[key] = self._store[key]
+ else:
+ logger.error("Unknown key '%s' in order", key)
+ return False
+
+ self._store = store
+
+ return True
def resetCounts(self):
"""Clear the counts of references to the status entries.
"""
- self._theCounts = [0]*self._theLength
+ for key in self._store:
+ self._store[key]["count"] = 0
return
- def countEntry(self, theLabel):
- """Increment the counter for a given label. This should be used
- together with resetCounts in a loop over project items.
+ def increment(self, key):
+ """Increment the counter for a given entry.
"""
- theIndex = self._getIndex(theLabel)
- if theIndex is not None:
- self._theCounts[theIndex] += 1
+ if key in self._store:
+ self._store[key]["count"] += 1
return
def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the
main project file.
"""
- for n in range(self._theLength):
+ for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={
- "red": str(self._theColours[n][0]),
- "green": str(self._theColours[n][1]),
- "blue": str(self._theColours[n][2]),
+ "key": key,
+ "count": str(data["count"]),
+ "red": str(data["cols"][0]),
+ "green": str(data["cols"][1]),
+ "blue": str(data["cols"][2]),
})
- xSub.text = self._theLabels[n]
+ xSub.text = data["name"]
+
return True
def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values.
"""
- theLabels = []
- theColours = []
+ self._store = {}
+ self._reverse = {}
+ self._default = None
for xChild in xParent:
- theLabels.append(xChild.text)
- cR = checkInt(xChild.attrib.get("red", 0), 0, False)
- cG = checkInt(xChild.attrib.get("green", 0), 0, False)
- cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
- theColours.append((cR, cG, cB))
-
- if len(theLabels) > 0:
- self._theLabels = []
- self._theColours = []
- self._theCounts = []
- self._theIcons = []
- self._theMap = {}
- self._theLength = 0
- self._theIndex = 0
-
- for n in range(len(theLabels)):
- self.addEntry(theLabels[n], theColours[n])
+ key = xChild.attrib.get("key", None)
+ name = xChild.text.strip()
+ count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
+ red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
+ green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
+ blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
+ self.write(key, name, (red, green, blue), count)
return True
@@ -165,39 +241,52 @@ class NWStatus():
# Internal Functions
##
- def _getIndex(self, theLabel):
- """Look up a status entry in the object lists, and return it if
- it exists.
+ def _newKey(self):
+ """Generate a new key for a status flag. This method is
+ recursive, but should only fail if there is an issue with the
+ random number generator or the user has added a lot of status
+ flags. The Python recursion limit is given the job to handle
+ the extreme case and will cause an app crash.
"""
- if theLabel is None:
- return None
- return self._theMap.get(theLabel.strip(), None)
+ key = f"{self._prefix}{random.getrandbits(24):06x}"
+ if key in self._store:
+ key = self._newKey()
+ return key
+
+ def _isKey(self, value):
+ """Check if a value is a key or not.
+ """
+ if not isinstance(value, str):
+ return False
+ if len(value) != 7:
+ return False
+ if value[0] != self._prefix:
+ return False
+ for c in value[1:]:
+ if c not in "0123456789abcdef":
+ return False
+ return True
##
# Iterator Bits
##
- def __getitem__(self, n):
- """Return an entry by its index.
- """
- if n >= 0 and n < self._theLength:
- return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n]
- return None, None, None, QIcon()
+ def __len__(self):
+ return len(self._store)
+
+ def __getitem__(self, key):
+ return self._store[key]
def __iter__(self):
- """Initialise the iterator.
- """
- self._theIndex = 0
- return self
+ return iter(self._store)
- def __next__(self):
- """Return the next entry for the iterator.
- """
- if self._theIndex < self._theLength:
- theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex)
- self._theIndex += 1
- return theLabel, theColour, theCount, theIcon
- else:
- raise StopIteration
+ def keys(self):
+ return self._store.keys()
+
+ def items(self):
+ return self._store.items()
+
+ def values(self):
+ return self._store.values()
# END Class NWStatus
diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py
index a39d93f4..84a0db14 100644
--- a/novelwriter/dialogs/itemeditor.py
+++ b/novelwriter/dialogs/itemeditor.py
@@ -75,11 +75,20 @@ class GuiItemEditor(QDialog):
self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL:
- for sLabel, _, _, sIcon in self.theProject.statusItems:
- self.editStatus.addItem(sIcon, sLabel, sLabel)
+ for key, entry in self.theProject.statusItems.items():
+ self.editStatus.addItem(entry["icon"], entry["name"], key)
+
+ index = self.editStatus.findData(self.theItem.itemStatus)
+ if index != -1:
+ self.editStatus.setCurrentIndex(index)
+
else:
- for sLabel, _, _, sIcon in self.theProject.importItems:
- self.editStatus.addItem(sIcon, sLabel, sLabel)
+ for key, entry in self.theProject.importItems.items():
+ self.editStatus.addItem(entry["icon"], entry["name"], key)
+
+ index = self.editStatus.findData(self.theItem.itemImport)
+ if index != -1:
+ self.editStatus.setCurrentIndex(index)
# Item Layout
self.editLayout = QComboBox()
@@ -97,6 +106,10 @@ class GuiItemEditor(QDialog):
if itemLayout in validLayouts:
self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout)
+ index = self.editLayout.findData(self.theItem.itemLayout)
+ if index != -1:
+ self.editLayout.setCurrentIndex(index)
+
# Export Switch
self.textExport = QLabel(self.tr("Include when building project"))
self.editExport = QSwitch()
@@ -116,15 +129,6 @@ class GuiItemEditor(QDialog):
self.editName.setText(self.theItem.itemName)
self.editName.selectAll()
- currStatus, _ = self.theItem.getImportStatus()
- statusIdx = self.editStatus.findData(currStatus)
- if statusIdx != -1:
- self.editStatus.setCurrentIndex(statusIdx)
-
- layoutIdx = self.editLayout.findData(self.theItem.itemLayout)
- if layoutIdx != -1:
- self.editLayout.setCurrentIndex(layoutIdx)
-
##
# Assemble
##
diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py
index f6e4b611..2a2bb969 100644
--- a/novelwriter/dialogs/projsettings.py
+++ b/novelwriter/dialogs/projsettings.py
@@ -35,6 +35,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwAlert
+from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
@@ -81,6 +82,9 @@ class GuiProjectSettings(PagedDialog):
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
+ # Flags
+ self.spellChanged = False
+
logger.debug("GuiProjectSettings initialisation complete")
return
@@ -103,16 +107,18 @@ class GuiProjectSettings(PagedDialog):
self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors)
- self.theProject.setSpellLang(spellLang)
self.theProject.setProjBackup(doBackup)
+ # Remember this as updating spell dictionary can be expensive
+ self.spellChanged = self.theProject.setSpellLang(spellLang)
+
if self.tabStatus.colChanged:
- statusCol = self.tabStatus.getNewList()
- self.theProject.setStatusColours(statusCol)
+ newList, delList = self.tabStatus.getNewList()
+ self.theProject.setStatusColours(newList, delList)
if self.tabImport.colChanged:
- importCol = self.tabImport.getNewList()
- self.theProject.setImportColours(importCol)
+ newList, delList = self.tabImport.getNewList()
+ self.theProject.setImportColours(newList, delList)
if self.tabStatus.colChanged or self.tabImport.colChanged:
self.theParent.rebuildTrees()
@@ -245,6 +251,10 @@ class GuiProjectEditStatus(QWidget):
COL_LABEL = 0
COL_USAGE = 1
+ KEY_ROLE = Qt.UserRole
+ COL_ROLE = Qt.UserRole + 1
+ NUM_ROLE = Qt.UserRole + 2
+
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
@@ -267,10 +277,9 @@ class GuiProjectEditStatus(QWidget):
self.optState.getInt("GuiProjectSettings", colSetting, 130)
)
- self.colData = []
- self.colCounts = []
+ self.colDeleted = []
self.colChanged = False
- self.selColour = None
+ self.selColour = QColor(100, 100, 100)
self.iPx = self.theTheme.baseIconSize
@@ -285,8 +294,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0)
- for iName, iCol, nUse, _ in self.theStatus:
- self._addItem(iName, iCol, iName, nUse)
+ for key, entry in self.theStatus.items():
+ self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls
# =============
@@ -297,6 +306,12 @@ class GuiProjectEditStatus(QWidget):
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._delItem)
+ self.upButton = QPushButton(self.theTheme.getIcon("up"), "")
+ self.upButton.clicked.connect(lambda: self._moveItem(-1))
+
+ self.dnButton = QPushButton(self.theTheme.getIcon("down"), "")
+ self.dnButton.clicked.connect(lambda: self._moveItem(1))
+
# Edit Form
# =========
@@ -306,7 +321,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.colPixmap = QPixmap(self.iPx, self.iPx)
- self.colPixmap.fill(QColor(120, 120, 120))
+ self.colPixmap.fill(QColor(100, 100, 100))
self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"))
self.colButton.setIconSize(self.colPixmap.rect().size())
self.colButton.clicked.connect(self._selectColour)
@@ -320,6 +335,8 @@ class GuiProjectEditStatus(QWidget):
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
+ self.listControls.addWidget(self.upButton)
+ self.listControls.addWidget(self.dnButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
@@ -349,12 +366,15 @@ class GuiProjectEditStatus(QWidget):
if self.colChanged:
newList = []
for n in range(self.listBox.topLevelItemCount()):
- nItem = self.listBox.topLevelItem(n)
- nIdx = nItem.data(self.COL_LABEL, Qt.UserRole)
- newList.append(self.colData[nIdx])
- return newList
+ item = self.listBox.topLevelItem(n)
+ newList.append({
+ "key": item.data(self.COL_LABEL, self.KEY_ROLE),
+ "name": item.text(self.COL_LABEL),
+ "cols": item.data(self.COL_LABEL, self.COL_ROLE),
+ })
+ return newList, self.colDeleted
- return None
+ return [], []
##
# User Actions
@@ -369,16 +389,16 @@ class GuiProjectEditStatus(QWidget):
)
if newCol.isValid():
self.selColour = newCol
- colPixmap = QPixmap(self.iPx, self.iPx)
- colPixmap.fill(newCol)
- self.colButton.setIcon(QIcon(colPixmap))
- self.colButton.setIconSize(colPixmap.rect().size())
+ pixmap = QPixmap(self.iPx, self.iPx)
+ pixmap.fill(newCol)
+ self.colButton.setIcon(QIcon(pixmap))
+ self.colButton.setIconSize(pixmap.rect().size())
return
def _newItem(self):
"""Create a new status item.
"""
- newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0)
+ newItem = self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70)))
newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70)))
self.colChanged = True
@@ -390,14 +410,14 @@ class GuiProjectEditStatus(QWidget):
selItem = self._getSelectedItem()
if selItem is not None:
iRow = self.listBox.indexOfTopLevelItem(selItem)
- selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
- if self.colCounts[selIdx] == 0:
- self.listBox.takeTopLevelItem(iRow)
- self.colChanged = True
- else:
+ if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.theParent.makeAlert(self.tr(
"Cannot delete a status item that is in use."
), nwAlert.ERROR)
+ else:
+ self.listBox.takeTopLevelItem(iRow)
+ self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
+ self.colChanged = True
return
def _saveItem(self):
@@ -405,53 +425,75 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
- selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
- self.colData[selIdx] = (
- self.editName.text().strip(),
- self.selColour.red(),
- self.selColour.green(),
- self.selColour.blue(),
- self.colData[selIdx][4]
- )
- selItem.setText(self.COL_LABEL, self.colData[selIdx][0])
- selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx]))
+ selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
+ selItem.setData(self.COL_LABEL, self.COL_ROLE, (
+ self.selColour.red(), self.selColour.green(), self.selColour.blue()
+ ))
self.editName.setEnabled(False)
self.colChanged = True
return
- def _addItem(self, iName, iCol, oName, nUse):
+ def _addItem(self, key, name, cols, count):
"""Add a status item to the list.
"""
- newIcon = QPixmap(self.iPx, self.iPx)
- newIcon.fill(QColor(*iCol))
- newItem = QTreeWidgetItem()
- newItem.setText(self.COL_LABEL, iName)
- newItem.setText(self.COL_USAGE, self._usageString(nUse))
- newItem.setIcon(self.COL_LABEL, QIcon(newIcon))
- newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData))
- self.listBox.addTopLevelItem(newItem)
- self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
- self.colCounts.append(nUse)
- return newItem
+ pixmap = QPixmap(self.iPx, self.iPx)
+ pixmap.fill(QColor(*cols))
+
+ item = QTreeWidgetItem()
+ item.setText(self.COL_LABEL, name)
+ item.setIcon(self.COL_LABEL, QIcon(pixmap))
+ item.setData(self.COL_LABEL, self.KEY_ROLE, key)
+ item.setData(self.COL_LABEL, self.COL_ROLE, cols)
+ item.setData(self.COL_LABEL, self.NUM_ROLE, count)
+ item.setText(self.COL_USAGE, self._usageString(count))
+
+ self.listBox.addTopLevelItem(item)
+
+ return item
+
+ def _moveItem(self, step):
+ """Move and item up or down step.
+ """
+ selItem = self._getSelectedItem()
+ if selItem is None:
+ return
+
+ tIndex = self.listBox.indexOfTopLevelItem(selItem)
+ nChild = self.listBox.topLevelItemCount()
+ nIndex = tIndex + step
+ if nIndex < 0 or nIndex >= nChild:
+ return
+
+ cItem = self.listBox.takeTopLevelItem(tIndex)
+ self.listBox.insertTopLevelItem(nIndex, cItem)
+ self.listBox.clearSelection()
+
+ cItem.setSelected(True)
+ self.colChanged = True
+
+ return
def _selectedItem(self):
"""Extract the info of a selected item and populate the settings
boxes and button.
"""
selItem = self._getSelectedItem()
- if selItem is not None:
- selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
- selVal = self.colData[selIdx]
- self.selColour = QColor(selVal[1], selVal[2], selVal[3])
- newIcon = QPixmap(self.iPx, self.iPx)
- newIcon.fill(self.selColour)
- self.editName.setText(selVal[0])
- self.colButton.setIcon(QIcon(newIcon))
- self.editName.setEnabled(True)
- self.editName.selectAll()
- self.editName.setFocus()
+ if selItem is None:
+ return
+
+ cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
+ name = selItem.text(self.COL_LABEL)
+
+ pixmap = QPixmap(self.iPx, self.iPx)
+ pixmap.fill(QColor(*cols))
+ self.selColour = QColor(*cols)
+ self.editName.setText(name)
+ self.colButton.setIcon(QIcon(pixmap))
+ self.editName.setEnabled(True)
+ self.editName.selectAll()
+ self.editName.setFocus()
return
@@ -467,12 +509,6 @@ class GuiProjectEditStatus(QWidget):
return selItem[0]
return None
- def _rowsMoved(self):
- """A row has been moved, so set the changed flag.
- """
- self.colChanged = True
- return
-
def _usageString(self, nUse):
"""Generate usage string.
"""
diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py
index 21df9b80..73082fa5 100644
--- a/novelwriter/gui/itemdetails.py
+++ b/novelwriter/gui/itemdetails.py
@@ -214,6 +214,11 @@ class GuiItemDetails(QWidget):
return
+ def refreshDetails(self):
+ """Reload the content of the details panel.
+ """
+ self.updateViewBox(self._itemHandle)
+
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py
index cc97cdf5..b45c298f 100644
--- a/novelwriter/gui/outlinedetails.py
+++ b/novelwriter/gui/outlinedetails.py
@@ -291,8 +291,10 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("%s" % self.tr("Title"))
self.titleValue.setText(novIdx["title"])
+ itemStatus, _ = nwItem.getImportStatus()
+
self.fileValue.setText(nwItem.itemName)
- self.itemValue.setText(nwItem.itemStatus)
+ self.itemValue.setText(itemStatus)
cC = checkInt(novIdx["cCount"], 0)
wC = checkInt(novIdx["wCount"], 0)
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 6cdf72ef..3c180b50 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -469,7 +469,7 @@ class GuiIcons:
"delete", "close", "done", "clear", "save", "add", "remove",
"search", "search_replace", "edit", "check", "cross", "hash",
"maximise", "minimise", "refresh", "reference", "backward",
- "forward", "settings",
+ "forward", "settings", "up", "down",
# Switches
"sticky-on", "sticky-off",
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 9db27659..c21fc091 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -545,7 +545,7 @@ class GuiMain(QMainWindow):
if self.theIndex.indexBroken:
self.makeAlert(self.tr(
"The project index is outdated or broken. Rebuilding index."
- ), nwAlert.WARN)
+ ), nwAlert.INFO)
self.rebuildIndex()
# Make sure the changed status is set to false on things opened
@@ -1002,7 +1002,9 @@ class GuiMain(QMainWindow):
if dlgProj.result() == QDialog.Accepted:
logger.debug("Applying new project settings")
- self.docEditor.setDictionaries()
+ if dlgProj.spellChanged:
+ self.docEditor.setDictionaries()
+ self.treeMeta.refreshDetails()
self._updateWindowTitle(self.theProject.projName)
return True
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 68265de5..002cea43 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -7,7 +7,7 @@
Jay Doh
1303
199
- 65005
+ 65049
False
@@ -33,121 +33,121 @@
- New
- Notes
- Started
- 1st Draft
- 2nd Draft
- 3rd Draft
- Finished
+ New
+ Notes
+ Started
+ 1st Draft
+ 2nd Draft
+ 3rd Draft
+ Finished
- None
- Minor
- Major
- Main
+ None
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Title Page
+ Title Page
-
- Page
+ Page
-
- Part One
+ Part One
-
- A Folder
+ A Folder
-
- Chapter One
+ Chapter One
-
- Making a Scene
+ Making a Scene
-
- Another Scene
+ Another Scene
-
- Interlude
+ Interlude
-
- A Note on Structure
+ A Note on Structure
-
- Chapter Two
+ Chapter Two
-
- We Found John!
+ We Found John!
-
- Characters
+ Characters
-
- Main Characters
+ Main Characters
-
- John Smith
+ John Smith
-
- Jane Smith
+ Jane Smith
-
- Locations
+ Locations
-
- Earth
+ Earth
-
- Space
+ Space
-
- Mars
+ Mars
-
- Archive
+ Archive
-
- Scenes
+ Scenes
-
- Old File
+ Old File
-
- Trash
+ Trash
-
- Delete Me!
+ Delete Me!
diff --git a/tests/conftest.py b/tests/conftest.py
index 4d8d7183..8551724b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -24,6 +24,8 @@ import sys
import pytest
import shutil
+from dataclasses import dataclass
+
from mock import MockGuiMain
from tools import cleanProject
@@ -249,9 +251,24 @@ def nwOldProj(tmpDir):
##
-# Useful Fixtures
+# Data Fixtures
##
+@dataclass
+class TestConst:
+
+ statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"]
+ importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"]
+
+
+@pytest.fixture(scope="session")
+def constData():
+ """A named tuple of known contstant values. For those that depend on
+ the random number generator, they assume the seed is 42.
+ """
+ return TestConst()
+
+
@pytest.fixture(scope="session")
def ipsumText():
"""Return five paragraphs of Lorem Ipsum text.
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index 05069e2c..d8d7a49e 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,12 +1,12 @@
-
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
- 23
+ 24
24
- 1854
+ 1856
False
@@ -31,102 +31,102 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Lorem Ipsum
+ Lorem Ipsum
-
- Front Matter
+ Front Matter
-
- Prologue
+ Prologue
-
- Act One
+ Act One
-
- Chapter One
+ Chapter One
-
- Chapter One
+ Chapter One
-
- Scene One
+ Scene One
-
- Scene Two
+ Scene Two
-
- Interlude
+ Interlude
-
- Chapter Two
+ Chapter Two
-
- Chapter Two
+ Chapter Two
-
- Scene Three
+ Scene Three
-
- Scene Four
+ Scene Four
-
- Scene Five
+ Scene Five
-
- Characters
+ Characters
-
- Mr. Nobody
+ Mr. Nobody
-
- Plot
+ Plot
-
- Main
+ Main
-
- World
+ World
-
- Ancient Europe
+ Ancient Europe
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx
index 82fa1ed1..6945dc08 100644
--- a/tests/minimal/nwProject.nwx
+++ b/tests/minimal/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Test Minimal
Minimal
Jane Doe
John Doh
- 14
+ 15
2
- 135
+ 146
True
@@ -29,50 +29,50 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx
index 92bbcf0e..2651909c 100644
--- a/tests/reference/coreProject_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -29,110 +29,110 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- Locations
+ Locations
-
- Timeline
+ Timeline
-
- Objects
+ Objects
-
- Entities
+ Entities
-
- Title Page
+ Title Page
-
- Chapter 1
+ Chapter 1
-
- Chapter 1
+ Chapter 1
-
- Scene 1.1
+ Scene 1.1
-
- Scene 1.2
+ Scene 1.2
-
- Scene 1.3
+ Scene 1.3
-
- Chapter 2
+ Chapter 2
-
- Chapter 2
+ Chapter 2
-
- Scene 2.1
+ Scene 2.1
-
- Scene 2.2
+ Scene 2.2
-
- Scene 2.3
+ Scene 2.3
-
- Chapter 3
+ Chapter 3
-
- Chapter 3
+ Chapter 3
-
- Scene 3.1
+ Scene 3.1
-
- Scene 3.2
+ Scene 3.2
-
- Scene 3.3
+ Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx
index e9ed7a7f..7ca8751d 100644
--- a/tests/reference/coreProject_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -29,74 +29,74 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- Locations
+ Locations
-
- Timeline
+ Timeline
-
- Objects
+ Objects
-
- Entities
+ Entities
-
- Title Page
+ Title Page
-
- Scene 1
+ Scene 1
-
- Scene 2
+ Scene 2
-
- Scene 3
+ Scene 3
-
- Scene 4
+ Scene 4
-
- Scene 5
+ Scene 5
-
- Scene 6
+ Scene 6
diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx
index 4cd389ba..a1c33b58 100644
--- a/tests/reference/coreProject_NewFile_nwProject.nwx
+++ b/tests/reference/coreProject_NewFile_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,58 +27,58 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Hello
+ Hello
-
- Jane
+ Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx
index 0bd9c54a..9c58c831 100644
--- a/tests/reference/coreProject_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,50 +27,50 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 74edeecd..4274814c 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,82 +27,82 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Novel
+ Novel
-
- Plot
+ Plot
-
- Character
+ Character
-
- World
+ World
-
- Timeline
+ Timeline
-
- Object
+ Object
-
- Custom1
+ Custom1
-
- Custom2
+ Custom2
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index c6e522b1..84515134 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,62 +27,62 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Plot
+ Plot
-
- New File
+ New File
-
- Characters
+ Characters
-
- New File
+ New File
-
- World
+ World
-
- New File
+ New File
-
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index a9f83ce2..8ed179b3 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,50 +27,50 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
-
- Novel
+ Novel
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
index ab60874a..b74533d4 100644
--- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx
+++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Project Name
Project Title
@@ -23,7 +23,7 @@
B
D
- With This Stuff
+ With This Stuff
%title%
@@ -33,50 +33,50 @@
- New
- Note
- Finished
- Final
+ New
+ Note
+ Finished
+ Final
- New
- Minor
- Major
- Final
+ New
+ Minor
+ Major
+ Final
-
- Novel
+ Novel
-
- Title Page
+ Title Page
-
- New Chapter
+ New Chapter
-
- New Chapter
+ New Chapter
-
- New Scene
+ New Scene
-
- Plot
+ Plot
-
- Characters
+ Characters
-
- World
+ World
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index f5eef8fe..d7ad4119 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -31,9 +31,9 @@ from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange,
- checkIntTuple, formatInt, formatTimeStamp, formatTime, splitVersionNumber,
- transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile,
- makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
+ minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified,
+ splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode,
+ readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
)
@@ -226,6 +226,16 @@ def testBaseCommon_CheckIntRange():
# END Test testBaseCommon_CheckIntRange
+@pytest.mark.base
+def testBaseCommon_MinMax():
+ """Test the minmax function.
+ """
+ for i in range(-5, 15):
+ assert 0 <= minmax(i, 0, 10) <= 10
+
+# END Test testBaseCommon_MinMax
+
+
@pytest.mark.base
def testBaseCommon_CheckIntTuple():
"""Test the checkIntTuple function.
@@ -270,6 +280,17 @@ def testBaseCommon_FormatTime():
# END Test testBaseCommon_FormatTime
+@pytest.mark.base
+def testBaseCommon_Simplified():
+ """Test the simplified function.
+ """
+ assert simplified("Hello World") == "Hello World"
+ assert simplified(" Hello World ") == "Hello World"
+ assert simplified("\tHello\n\r\tWorld") == "Hello World"
+
+# END Test testBaseCommon_Simplified
+
+
@pytest.mark.base
def testBaseCommon_SplitVersionNumber():
"""Test the splitVersionNumber function.
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py
index 874177e1..b86fe968 100644
--- a/tests/test_core/test_core_item.py
+++ b/tests/test_core/test_core_item.py
@@ -20,6 +20,7 @@ along with this program. If not, see .
"""
import pytest
+import random
from lxml import etree
@@ -31,9 +32,10 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@pytest.mark.core
-def testCoreItem_Setters(mockGUI):
+def testCoreItem_Setters(mockGUI, constData):
"""Test all the simple setters for the NWItem class.
"""
+ random.seed(42)
theProject = NWProject(mockGUI)
theItem = NWItem(theProject)
@@ -42,6 +44,8 @@ def testCoreItem_Setters(mockGUI):
assert theItem.itemName == "A Name"
theItem.setName("\t A Name ")
assert theItem.itemName == "A Name"
+ theItem.setName("\t A\t\u2009\u202f\u2002\u2003\u2028\u2029Name ")
+ assert theItem.itemName == "A Name"
theItem.setName(123)
assert theItem.itemName == ""
@@ -89,50 +93,32 @@ def testCoreItem_Setters(mockGUI):
# Importance
theItem._class = nwItemClass.CHARACTER
- theItem.setImport("Nonsense")
- assert theItem.itemImport == "New"
- theItem.setImport("New")
- assert theItem.itemImport == "New"
- theItem.setImport("Minor")
- assert theItem.itemImport == "Minor"
- theItem.setImport("Major")
- assert theItem.itemImport == "Major"
- theItem.setImport("Main")
- assert theItem.itemImport == "Main"
+ theItem.setImport("Word")
+ assert theItem.itemImport == constData.importKeys[0] # Default
+ for key in constData.importKeys:
+ theItem.setImport(key)
+ assert theItem.itemImport == key
# Status
theItem._class = nwItemClass.NOVEL
- theItem.setStatus("Nonsense")
- assert theItem.itemStatus == "New"
- theItem.setStatus("New")
- assert theItem.itemStatus == "New"
- theItem.setStatus("Note")
- assert theItem.itemStatus == "Note"
- theItem.setStatus("Draft")
- assert theItem.itemStatus == "Draft"
- theItem.setStatus("Finished")
- assert theItem.itemStatus == "Finished"
+ theItem.setStatus("Word")
+ assert theItem.itemStatus == constData.statusKeys[0] # Default
+ for key in constData.statusKeys:
+ theItem.setStatus(key)
+ assert theItem.itemStatus == key
# Status/Importance Wrapper
theItem._class = nwItemClass.CHARACTER
- theItem.setImportStatus("New")
- assert theItem.itemImport == "New"
- theItem.setImportStatus("Minor")
- assert theItem.itemImport == "Minor"
- theItem.setImportStatus("Note")
- assert theItem.itemImport == "New"
- theItem.setImportStatus("Draft")
- assert theItem.itemImport == "New"
+ for key in constData.importKeys:
+ theItem.setImport(key)
+ assert theItem.itemImport == key
+ assert theItem.itemStatus == constData.statusKeys[3] # Should not change
theItem._class = nwItemClass.NOVEL
- theItem.setImportStatus("New")
- assert theItem.itemStatus == "New"
- theItem.setImportStatus("Minor")
- assert theItem.itemStatus == "New"
- theItem.setImportStatus("Note")
- assert theItem.itemStatus == "Note"
- theItem.setImportStatus("Draft")
- assert theItem.itemStatus == "Draft"
+ for key in constData.statusKeys:
+ theItem.setStatus(key)
+ assert theItem.itemImport == constData.importKeys[3] # Should not change
+ assert theItem.itemStatus == key
# Expanded
theItem.setExpanded(8)
@@ -366,9 +352,10 @@ def testCoreItem_LayoutSetter(mockGUI):
@pytest.mark.core
-def testCoreItem_XMLPackUnpack(mockGUI, caplog):
+def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
"""Test packing and unpacking XML objects for the NWItem class.
"""
+ random.seed(42)
theProject = NWProject(mockGUI)
nwXML = etree.Element("novelWriterXML")
@@ -383,7 +370,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
theItem.setName("A Name")
theItem.setClass("NOVEL")
theItem.setType("FILE")
- theItem.setStatus("Main")
+ theItem.setImport(constData.importKeys[3])
theItem.setLayout("NOTE")
theItem.setExported(False)
theItem.setParaCount(3)
@@ -398,15 +385,16 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
b''
b'- A Name
'
+ b'cursorPos="11"/>A Name '
b''
- )
+ ) % bytes(constData.importKeys[3], encoding="utf8")
# Unpack
theItem = NWItem(theProject)
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
@@ -416,6 +404,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FILE
assert theItem.itemLayout == nwItemLayout.NOTE
+ assert theItem.itemStatus == constData.statusKeys[0] # Was None, should now be default
+ assert theItem.itemImport == constData.importKeys[3]
# Folder
# ======
@@ -428,7 +418,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
theItem.setName("A Name")
theItem.setClass("NOVEL")
theItem.setType("FOLDER")
- theItem.setStatus("Main")
+ theItem.setStatus(constData.statusKeys[1])
theItem.setLayout("NOTE")
theItem.setExpanded(True)
theItem.setExported(False)
@@ -443,16 +433,17 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b''
b'- A Name
'
b''
- )
+ ) % bytes(constData.statusKeys[1], encoding="utf8")
# Unpack
theItem = NWItem(theProject)
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
@@ -463,6 +454,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FOLDER
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
+ assert theItem.itemStatus == constData.statusKeys[1]
+ assert theItem.itemImport == constData.importKeys[0] # Was None, should now be default
# Errors
# ======
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index bfcc5171..30cb7f33 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import pytest
import os
+import pytest
+import random
from shutil import copyfile
from zipfile import ZipFile
@@ -44,6 +45,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
testFile = os.path.join(outDir, "coreProject_NewMinimal_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx")
+ random.seed(42)
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
@@ -112,6 +114,7 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
"numScenes": 3,
"chFolders": True,
}
+ random.seed(42)
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
@@ -154,6 +157,7 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
"numScenes": 6,
"chFolders": True,
}
+ random.seed(42)
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
@@ -262,6 +266,7 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
testFile = os.path.join(outDir, "coreProject_NewRoot_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
+ random.seed(42)
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
@@ -299,6 +304,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx")
+ random.seed(42)
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
@@ -680,13 +686,127 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
@pytest.mark.core
-def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
+def testCoreProject_StatusImport(mockGUI, fncDir, constData):
+ """Test the status and importance flag handling.
+ """
+ theProject = NWProject(mockGUI)
+ random.seed(42)
+ theProject.projTree.setSeed(42)
+ assert theProject.newProject({"projPath": fncDir}) is True
+
+ # Change Status
+ # =============
+
+ theProject.projTree["44cb730c42048"].setStatus("Finished")
+ theProject.projTree["71ee45a3c0db9"].setStatus("Draft")
+ theProject.projTree["811786ad1ae74"].setStatus("Note")
+ theProject.projTree["25fc0e7096fc6"].setStatus("Finished")
+
+ assert theProject.projTree["44cb730c42048"].itemStatus == constData.statusKeys[3]
+ assert theProject.projTree["71ee45a3c0db9"].itemStatus == constData.statusKeys[2]
+ assert theProject.projTree["811786ad1ae74"].itemStatus == constData.statusKeys[1]
+ assert theProject.projTree["25fc0e7096fc6"].itemStatus == constData.statusKeys[3]
+
+ newList = [
+ {"key": constData.statusKeys[0], "name": "New", "cols": (1, 1, 1)},
+ {"key": constData.statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped
+ {"key": constData.statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped
+ {"key": constData.statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed
+ {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name
+ ]
+ assert theProject.setStatusColours(None, None) is False
+ assert theProject.setStatusColours([], []) is False
+ assert theProject.setStatusColours(newList, []) is True
+
+ assert theProject.statusItems.name(constData.statusKeys[0]) == "New"
+ assert theProject.statusItems.name(constData.statusKeys[1]) == "Draft"
+ assert theProject.statusItems.name(constData.statusKeys[2]) == "Note"
+ assert theProject.statusItems.name(constData.statusKeys[3]) == "Edited"
+ assert theProject.statusItems.cols(constData.statusKeys[0]) == (1, 1, 1)
+ assert theProject.statusItems.cols(constData.statusKeys[1]) == (2, 2, 2)
+ assert theProject.statusItems.cols(constData.statusKeys[2]) == (3, 3, 3)
+ assert theProject.statusItems.cols(constData.statusKeys[3]) == (4, 4, 4)
+
+ # Check the new entry
+ lastKey = theProject.statusItems.check("Finished")
+ assert lastKey == "sbc8960"
+ assert theProject.statusItems.name(lastKey) == "Finished"
+ assert theProject.statusItems.cols(lastKey) == (5, 5, 5)
+
+ # Delete last entry
+ assert theProject.setStatusColours([], [lastKey]) is True
+ assert theProject.statusItems.name(lastKey) == "New"
+
+ # Change Importance
+ # =================
+
+ fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568")
+ theProject.projTree[fHandle].setImport("Main")
+
+ assert theProject.projTree[fHandle].itemImport == constData.importKeys[3]
+ newList = [
+ {"key": constData.importKeys[0], "name": "New", "cols": (1, 1, 1)},
+ {"key": constData.importKeys[1], "name": "Minor", "cols": (2, 2, 2)},
+ {"key": constData.importKeys[2], "name": "Major", "cols": (3, 3, 3)},
+ {"key": constData.importKeys[3], "name": "Min", "cols": (4, 4, 4)},
+ {"key": None, "name": "Max", "cols": (5, 5, 5)},
+ ]
+ assert theProject.setImportColours(None, None) is False
+ assert theProject.setImportColours([], []) is False
+ assert theProject.setImportColours(newList, []) is True
+
+ assert theProject.importItems.name(constData.importKeys[0]) == "New"
+ assert theProject.importItems.name(constData.importKeys[1]) == "Minor"
+ assert theProject.importItems.name(constData.importKeys[2]) == "Major"
+ assert theProject.importItems.name(constData.importKeys[3]) == "Min"
+ assert theProject.importItems.cols(constData.importKeys[0]) == (1, 1, 1)
+ assert theProject.importItems.cols(constData.importKeys[1]) == (2, 2, 2)
+ assert theProject.importItems.cols(constData.importKeys[2]) == (3, 3, 3)
+ assert theProject.importItems.cols(constData.importKeys[3]) == (4, 4, 4)
+
+ # Check the new entry
+ lastKey = theProject.importItems.check("Max")
+ assert lastKey == "i1a3d1f"
+ assert theProject.importItems.name(lastKey) == "Max"
+ assert theProject.importItems.cols(lastKey) == (5, 5, 5)
+
+ # Delete last entry
+ assert theProject.setImportColours([], [lastKey]) is True
+ assert theProject.importItems.name(lastKey) == "New"
+
+ # Delete Status/Import
+ # ====================
+
+ theProject.statusItems.resetCounts()
+ for key in list(theProject.statusItems.keys()):
+ assert theProject.statusItems.remove(key) is True
+
+ theProject.importItems.resetCounts()
+ for key in list(theProject.importItems.keys()):
+ assert theProject.importItems.remove(key) is True
+
+ assert len(theProject.statusItems) == 0
+ assert len(theProject.importItems) == 0
+ assert theProject.saveProject() is True
+ assert theProject.closeProject() is True
+
+ # This should restore the default status/import labels
+ random.seed(42)
+ assert theProject.openProject(fncDir) is True
+ assert theProject.saveProject() is True
+ assert list(theProject.statusItems.keys()) == constData.statusKeys
+ assert list(theProject.importItems.keys()) == constData.importKeys
+
+# END Test testCoreProject_StatusImport
+
+
+@pytest.mark.core
+def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir):
"""Test other project class methods and functions.
"""
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
- assert theProject.openProject(nwMinimal)
- assert theProject.projPath == nwMinimal
+ assert theProject.newProject({"projPath": fncDir}) is True
# Setting project path
assert theProject.setProjectPath(None)
@@ -697,16 +817,16 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert theProject.projPath == os.path.expanduser("~")
# Create a new folder and populate it
- projPath = os.path.join(nwMinimal, "mock1")
+ projPath = os.path.join(fncDir, "mock1")
assert theProject.setProjectPath(projPath, newProject=True)
# Make os.mkdir fail
monkeypatch.setattr("os.mkdir", causeOSError)
- projPath = os.path.join(nwMinimal, "mock2")
+ projPath = os.path.join(fncDir, "mock2")
assert not theProject.setProjectPath(projPath, newProject=True)
# Set back
- assert theProject.setProjectPath(nwMinimal)
+ assert theProject.setProjectPath(fncDir)
# Project Name
assert theProject.setProjectName(" A Name ")
@@ -744,9 +864,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Trash folder
# Should create on first call, and just returned on later calls
- assert theProject.projTree["73475cb40a568"] is None
- assert theProject.trashFolder() == "73475cb40a568"
- assert theProject.trashFolder() == "73475cb40a568"
+ hTrash = "1a6562590ef19"
+ assert theProject.projTree[hTrash] is None
+ assert theProject.trashFolder() == hTrash
+ assert theProject.trashFolder() == hTrash
# Project backup
assert theProject.doBackup is True
@@ -770,9 +891,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Spell language
theProject.projChanged = False
- assert theProject.setSpellLang(None)
assert theProject.projSpell is None
- assert theProject.setSpellLang("None")
+ assert theProject.setSpellLang(None) is False
+ assert theProject.projSpell is None
+ assert theProject.setSpellLang("None") is False # Should be interpreded as None
assert theProject.projSpell is None
assert theProject.setSpellLang("en_GB")
assert theProject.projSpell == "en_GB"
@@ -812,14 +934,14 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Change project tree order
oldOrder = [
- "a508bb932959c", "a35baf2e93843", "a6d311a93600a",
- "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
- "afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568",
+ "73475cb40a568", "44cb730c42048", "71ee45a3c0db9",
+ "811786ad1ae74", "25fc0e7096fc6", "31489056e0916",
+ "98010bd9270f9", "0e17daca5f3e1", "1a6562590ef19",
]
newOrder = [
- "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
- "a508bb932959c", "a35baf2e93843", "a6d311a93600a",
- "afb3043c7b2b3", "9d5247ab588e0",
+ "811786ad1ae74", "25fc0e7096fc6", "31489056e0916",
+ "73475cb40a568", "44cb730c42048", "71ee45a3c0db9",
+ "98010bd9270f9", "0e17daca5f3e1",
]
assert theProject.projTree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
@@ -827,56 +949,6 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert theProject.setTreeOrder(oldOrder)
assert theProject.projTree.handles() == oldOrder
- # Change status
- theProject.projTree["a35baf2e93843"].setStatus("Finished")
- theProject.projTree["a6d311a93600a"].setStatus("Draft")
- theProject.projTree["f5ab3e30151e1"].setStatus("Note")
- theProject.projTree["8c659a11cd429"].setStatus("Finished")
- newList = [
- ("New", 1, 1, 1, "New"),
- ("Draft", 2, 2, 2, "Note"), # These are swapped
- ("Note", 3, 3, 3, "Draft"), # These are swapped
- ("Edited", 4, 4, 4, "Finished"), # Renamed
- ("Finished", 5, 5, 5, None), # New, with reused name
- ]
- assert theProject.setStatusColours(newList)
- assert theProject.statusItems._theLabels == [
- "New", "Draft", "Note", "Edited", "Finished"
- ]
- assert theProject.statusItems._theColours == [
- (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
- ]
- assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed
- assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped
- assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped
- assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed
-
- # Change importance
- fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
- theProject.projTree[fHandle].setImport("Main")
- newList = [
- ("New", 1, 1, 1, "New"),
- ("Minor", 2, 2, 2, "Minor"),
- ("Major", 3, 3, 3, "Major"),
- ("Min", 4, 4, 4, "Main"),
- ("Max", 5, 5, 5, None),
- ]
- assert theProject.setImportColours(newList)
- assert theProject.importItems._theLabels == [
- "New", "Minor", "Major", "Min", "Max"
- ]
- assert theProject.importItems._theColours == [
- (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
- ]
- assert theProject.projTree[fHandle].itemImport == "Min"
-
- # Check status counts
- assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0]
- assert theProject.importItems._theCounts == [0, 0, 0, 0, 0]
- theProject.countStatus()
- assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0]
- assert theProject.importItems._theCounts == [3, 0, 0, 1, 0]
-
# Session stats
theProject.currWCount = 200
theProject.lastWCount = 100
@@ -890,7 +962,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert not theProject._appendSessionStats(idleTime=0)
# Write entry
- assert theProject.projMeta == os.path.join(nwMinimal, "meta")
+ assert theProject.projMeta == os.path.join(fncDir, "meta")
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
theProject.projOpened = 1600002000
diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py
index a7a2d55e..e1d224d8 100644
--- a/tests/test_core/test_core_status.py
+++ b/tests/test_core/test_core_status.py
@@ -20,6 +20,7 @@ along with this program. If not, see .
"""
import pytest
+import random
from lxml import etree
@@ -29,103 +30,300 @@ from novelwriter.core.status import NWStatus
@pytest.mark.core
-def testCoreStatus_Entries():
- """Test all the simple setters for the NWItem class.
+def testCoreStatus_Internal(constData):
+ """Test all the internal functions of the NWStatus class.
"""
- theStatus = NWStatus()
+ random.seed(42)
+ theStatus = NWStatus(NWStatus.STATUS)
+ theImport = NWStatus(NWStatus.IMPORT)
- # Add entries
- theStatus.addEntry("New", (100, 100, 100))
- theStatus.addEntry("Minor", (200, 50, 0))
- theStatus.addEntry("Major", (200, 150, 0))
- theStatus.addEntry("Main", (50, 200, 0))
+ with pytest.raises(Exception):
+ NWStatus(999)
- assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
- assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
- assert theStatus._theCounts == [0, 0, 0, 0]
- assert theStatus._theMap["New"] == 0
- assert theStatus._theMap["Minor"] == 1
- assert theStatus._theMap["Major"] == 2
- assert theStatus._theMap["Main"] == 3
- assert theStatus._theLength == 4
+ # Generate Key
+ # ============
- # Lookups
- assert theStatus._getIndex(None) is None
- assert theStatus._getIndex("stuff") is None
- assert theStatus._getIndex("Main") == 3
+ assert theStatus._newKey() == constData.statusKeys[0]
+ assert theStatus._newKey() == constData.statusKeys[1]
- # Checks
- assert theStatus.checkEntry(123) == "New"
- assert theStatus.checkEntry("Stuff") == "New"
- assert theStatus.checkEntry("New ") == "New"
- assert theStatus.checkEntry(" Main ") == "Main"
+ # Key collision, should move to key 3
+ theStatus.write(constData.statusKeys[2], "Crash", (0, 0, 0))
+ assert theStatus._newKey() == constData.statusKeys[3]
- # Icons
- assert isinstance(theStatus.getIcon("Stuff"), QIcon)
- assert isinstance(theStatus.getIcon("New"), QIcon)
+ assert theImport._newKey() == constData.importKeys[0]
+ assert theImport._newKey() == constData.importKeys[1]
- # Set new list
- newList = [
- ("New", 1, 1, 1, "New"),
- ("Minor", 2, 2, 2, "Minor"),
- ("Major", 3, 3, 3, "Major"),
- ("Min", 4, 4, 4, "Main"),
- ("Max", 5, 5, 5, None),
- ]
- assert theStatus.setNewEntries(None) == {}
- assert theStatus.setNewEntries(newList) == {"Main": "Min"}
+ # Key collision, should move to key 3
+ theImport.write(constData.importKeys[2], "Crash", (0, 0, 0))
+ assert theImport._newKey() == constData.importKeys[3]
- assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"]
- assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)]
- assert theStatus._theCounts == [0, 0, 0, 0, 0]
- assert theStatus._theMap["New"] == 0
- assert theStatus._theMap["Minor"] == 1
- assert theStatus._theMap["Major"] == 2
- assert theStatus._theMap["Min"] == 3
- assert theStatus._theMap["Max"] == 4
- assert theStatus._theLength == 5
+ # Check Key
+ # =========
- # Add counts
- countTo = [3, 5, 7, 9, 11]
- for i, n in enumerate(countTo):
- for _ in range(n):
- theStatus.countEntry(theStatus._theLabels[i])
- assert theStatus._theCounts == countTo
+ assert theStatus._isKey(None) is False # Not a string
+ assert theStatus._isKey("s00000") is False # Too short
+ assert theStatus._isKey("s000000") is True # Correct length
+ assert theStatus._isKey("s0000000") is False # Too long
+ assert theStatus._isKey("i000000") is False # Wrong type
+ assert theStatus._isKey("q000000") is False # Wrong type
+ assert theStatus._isKey("s12345H") is False # Not a hex value
+ assert theStatus._isKey("s12345F") is False # Not a lower case hex value
+ assert theStatus._isKey("s12345f") is True # Valid hex value
+
+ assert theImport._isKey(None) is False # Not a string
+ assert theImport._isKey("i00000") is False # Too short
+ assert theImport._isKey("i000000") is True # Correct length
+ assert theImport._isKey("i0000000") is False # Too long
+ assert theImport._isKey("s000000") is False # Wrong type
+ assert theImport._isKey("q000000") is False # Wrong type
+ assert theImport._isKey("i12345H") is False # Not a hex value
+ assert theImport._isKey("i12345F") is False # Not a lower case hex value
+ assert theImport._isKey("i12345f") is True # Valid hex value
+
+# END Test testCoreStatus_Internal
+
+
+@pytest.mark.core
+def testCoreStatus_Iterator(constData):
+ """Test the iterator functions of the NWStatus class.
+ """
+ random.seed(42)
+ theStatus = NWStatus(NWStatus.STATUS)
+ theStatus.write(None, "New", (100, 100, 100))
+ theStatus.write(None, "Note", (200, 50, 0))
+ theStatus.write(None, "Draft", (200, 150, 0))
+ theStatus.write(None, "Finished", (50, 200, 0))
+
+ # Direct access
+ entry = theStatus[constData.statusKeys[0]]
+ assert entry["cols"] == (100, 100, 100)
+ assert entry["name"] == "New"
+ assert entry["count"] == 0
+ assert isinstance(entry["icon"], QIcon)
# Iterate
- for i, (sA, sB, sC, sD) in enumerate(theStatus):
- assert sA == theStatus._theLabels[i]
- assert sB == theStatus._theColours[i]
- assert sC == theStatus._theCounts[i]
- assert sD == theStatus._theIcons[i]
+ entries = list(theStatus)
+ assert len(entries) == 4
+ assert len(theStatus) == 4
- sA, sB, sC, sD = theStatus[9]
- assert sA is None
- assert sB is None
- assert sC is None
- assert isinstance(sD, QIcon)
+ # Keys
+ assert list(theStatus.keys()) == constData.statusKeys
+
+ # Items
+ for index, (key, entry) in enumerate(theStatus.items()):
+ assert key == constData.statusKeys[index]
+ assert "cols" in entry
+ assert "name" in entry
+ assert "count" in entry
+ assert "icon" in entry
+
+ # Valuse
+ for entry in theStatus.values():
+ assert "cols" in entry
+ assert "name" in entry
+ assert "count" in entry
+ assert "icon" in entry
+
+# END Test testCoreStatus_Iterator
+
+
+@pytest.mark.core
+def testCoreStatus_Entries(constData):
+ """Test all the simple setters for the NWStatus class.
+ """
+ random.seed(42)
+ theStatus = NWStatus(NWStatus.STATUS)
+
+ # Write
+ # =====
+
+ # Have a key
+ theStatus.write(constData.statusKeys[0], "Entry 1", (200, 100, 50))
+ assert theStatus[constData.statusKeys[0]]["name"] == "Entry 1"
+ assert theStatus[constData.statusKeys[0]]["cols"] == (200, 100, 50)
+
+ # Don't have a key
+ theStatus.write(None, "Entry 2", (210, 110, 60))
+ assert theStatus[constData.statusKeys[1]]["name"] == "Entry 2"
+ assert theStatus[constData.statusKeys[1]]["cols"] == (210, 110, 60)
+
+ # Wrong colour spec
+ theStatus.write(None, "Entry 3", "what?")
+ assert theStatus[constData.statusKeys[2]]["name"] == "Entry 3"
+ assert theStatus[constData.statusKeys[2]]["cols"] == (100, 100, 100)
+
+ # Wrong colour count
+ theStatus.write(None, "Entry 4", (10, 20))
+ assert theStatus[constData.statusKeys[3]]["name"] == "Entry 4"
+ assert theStatus[constData.statusKeys[3]]["cols"] == (100, 100, 100)
+
+ # Check reverse map
+ assert theStatus._reverse == {
+ "Entry 1": constData.statusKeys[0],
+ "Entry 2": constData.statusKeys[1],
+ "Entry 3": constData.statusKeys[2],
+ "Entry 4": constData.statusKeys[3],
+ }
+
+ # Check
+ # =====
+
+ # Normal lookup
+ for key in constData.statusKeys:
+ assert theStatus.check(key) == key
+
+ # Reverse map lookup
+ assert theStatus.check("Entry 1") == constData.statusKeys[0]
+ assert theStatus.check("Entry 2") == constData.statusKeys[1]
+ assert theStatus.check("Entry 3") == constData.statusKeys[2]
+ assert theStatus.check("Entry 4") == constData.statusKeys[3]
+
+ # Non-existing name
+ assert theStatus.check("Entry 5") == constData.statusKeys[0]
+
+ # Name Access
+ # ===========
+
+ assert theStatus.name(constData.statusKeys[0]) == "Entry 1"
+ assert theStatus.name(constData.statusKeys[1]) == "Entry 2"
+ assert theStatus.name(constData.statusKeys[2]) == "Entry 3"
+ assert theStatus.name(constData.statusKeys[3]) == "Entry 4"
+ assert theStatus.name("blablabla") == "Entry 1"
+
+ # Colour Access
+ # =============
+
+ assert theStatus.cols(constData.statusKeys[0]) == (200, 100, 50)
+ assert theStatus.cols(constData.statusKeys[1]) == (210, 110, 60)
+ assert theStatus.cols(constData.statusKeys[2]) == (100, 100, 100)
+ assert theStatus.cols(constData.statusKeys[3]) == (100, 100, 100)
+ assert theStatus.cols("blablabla") == (200, 100, 50)
+
+ # Icon Access
+ # ===========
+
+ assert isinstance(theStatus.icon(constData.statusKeys[0]), QIcon)
+ assert isinstance(theStatus.icon(constData.statusKeys[1]), QIcon)
+ assert isinstance(theStatus.icon(constData.statusKeys[2]), QIcon)
+ assert isinstance(theStatus.icon(constData.statusKeys[3]), QIcon)
+ assert isinstance(theStatus.icon("blablabla"), QIcon)
+
+ # Increment and Count Access
+ # ==========================
+
+ countTo = [3, 5, 7, 9]
+ for i, n in enumerate(countTo):
+ for _ in range(n):
+ theStatus.increment(constData.statusKeys[i])
+
+ assert theStatus.count(constData.statusKeys[0]) == countTo[0]
+ assert theStatus.count(constData.statusKeys[1]) == countTo[1]
+ assert theStatus.count(constData.statusKeys[2]) == countTo[2]
+ assert theStatus.count(constData.statusKeys[3]) == countTo[3]
+ assert theStatus.count("blablabla") == countTo[0]
- # Clear counts
theStatus.resetCounts()
- assert theStatus._theCounts == [0, 0, 0, 0, 0]
+
+ assert theStatus.count(constData.statusKeys[0]) == 0
+ assert theStatus.count(constData.statusKeys[1]) == 0
+ assert theStatus.count(constData.statusKeys[2]) == 0
+ assert theStatus.count(constData.statusKeys[3]) == 0
+
+ # Reorder
+ # =======
+
+ cOrder = list(theStatus.keys())
+ assert cOrder == constData.statusKeys
+
+ # Wrong length
+ assert theStatus.reorder([]) is False
+
+ # No change
+ assert theStatus.reorder(cOrder) is False
+
+ # Actual reaorder
+ nOrder = [
+ constData.statusKeys[0],
+ constData.statusKeys[2],
+ constData.statusKeys[1],
+ constData.statusKeys[3],
+ ]
+ assert theStatus.reorder(nOrder) is True
+ assert list(theStatus.keys()) == nOrder
+
+ # Add an unknown key
+ wOrder = nOrder.copy()
+ wOrder[3] = theStatus._newKey()
+ assert theStatus.reorder(wOrder) is False
+ assert list(theStatus.keys()) == nOrder
+
+ # Put it back
+ assert theStatus.reorder(cOrder) is True
+ assert list(theStatus.keys()) == cOrder
+
+ # Default
+ # =======
+
+ default = theStatus._default
+ theStatus._default = None
+
+ assert theStatus.check("Entry 5") == ""
+ assert theStatus.name("blablabla") == ""
+ assert theStatus.cols("blablabla") == (100, 100, 100)
+ assert theStatus.count("blablabla") == 0
+ assert isinstance(theStatus.icon("blablabla"), QIcon)
+
+ theStatus._default = default
+
+ # Remove
+ # ======
+
+ # Non-existing entry
+ assert theStatus.remove("blablabla") is False
+
+ # Non-zero entry
+ theStatus.increment(constData.statusKeys[3])
+ assert theStatus.remove(constData.statusKeys[3]) is False
+
+ # Delete last entry
+ theStatus.resetCounts()
+ lastName = theStatus.name(constData.statusKeys[3])
+ assert lastName == "Entry 4"
+ assert theStatus.remove(constData.statusKeys[3]) is True
+ assert theStatus.check(constData.statusKeys[3]) == theStatus._default
+ assert theStatus.check(lastName) == theStatus._default
+
+ # Delete default entry, Entry 2 is new default
+ firstName = theStatus.name(theStatus._default)
+ assert firstName == "Entry 1"
+ assert theStatus.remove(theStatus._default) is True
+ assert theStatus.name(firstName) == "Entry 2"
+
+ # Remove remaining entries
+ assert theStatus.remove(constData.statusKeys[1]) is True
+ assert theStatus.remove(constData.statusKeys[2]) is True
+
+ assert len(theStatus) == 0
+ assert theStatus._default is None
# END Test testCoreStatus_Entries
@pytest.mark.core
-def testCoreStatus_XMLPackUnpack():
- """Test all the simple setters for the NWItem class.
+def testCoreStatus_XMLPackUnpack(constData):
+ """Test all the XML pack/unpack of the NWStatus class.
"""
- theStatus = NWStatus()
- theStatus.addEntry("New", (100, 100, 100))
- theStatus.addEntry("Minor", (200, 50, 0))
- theStatus.addEntry("Major", (200, 150, 0))
- theStatus.addEntry("Main", (50, 200, 0))
+ random.seed(42)
+ theStatus = NWStatus(NWStatus.STATUS)
+ theStatus.write(None, "New", (100, 100, 100))
+ theStatus.write(None, "Note", (200, 50, 0))
+ theStatus.write(None, "Draft", (200, 150, 0))
+ theStatus.write(None, "Finished", (50, 200, 0))
countTo = [3, 5, 7, 9]
for i, n in enumerate(countTo):
for _ in range(n):
- theStatus.countEntry(theStatus._theLabels[i])
+ theStatus.increment(constData.statusKeys[i])
nwXML = etree.Element("novelWriterXML")
@@ -134,23 +332,29 @@ def testCoreStatus_XMLPackUnpack():
theStatus.packXML(xStatus)
assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == (
b''
- b'New'
- b'Minor'
- b'Major'
- b'Main'
+ b'New'
+ b'Note'
+ b'Draft'
+ b'Finished'
b''
)
# Unpack
- theStatus = NWStatus()
+ theStatus = NWStatus(NWStatus.STATUS)
assert theStatus.unpackXML(xStatus)
- assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
- assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
- assert theStatus._theCounts == [0, 0, 0, 0]
- assert theStatus._theMap["New"] == 0
- assert theStatus._theMap["Minor"] == 1
- assert theStatus._theMap["Major"] == 2
- assert theStatus._theMap["Main"] == 3
- assert theStatus._theLength == 4
+ assert len(theStatus._store) == 4
+ assert list(theStatus._store.keys()) == constData.statusKeys
+ assert theStatus._store[constData.statusKeys[0]]["name"] == "New"
+ assert theStatus._store[constData.statusKeys[1]]["name"] == "Note"
+ assert theStatus._store[constData.statusKeys[2]]["name"] == "Draft"
+ assert theStatus._store[constData.statusKeys[3]]["name"] == "Finished"
+ assert theStatus._store[constData.statusKeys[0]]["cols"] == (100, 100, 100)
+ assert theStatus._store[constData.statusKeys[1]]["cols"] == (200, 50, 0)
+ assert theStatus._store[constData.statusKeys[2]]["cols"] == (200, 150, 0)
+ assert theStatus._store[constData.statusKeys[3]]["cols"] == (50, 200, 0)
+ assert theStatus._store[constData.statusKeys[0]]["count"] == countTo[0]
+ assert theStatus._store[constData.statusKeys[1]]["count"] == countTo[1]
+ assert theStatus._store[constData.statusKeys[2]]["count"] == countTo[2]
+ assert theStatus._store[constData.statusKeys[3]]["count"] == countTo[3]
# END Test testCoreStatus_XMLPackUnpack
diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py
index 79038345..10d72549 100644
--- a/tests/test_dialogs/test_dlg_itemeditor.py
+++ b/tests/test_dialogs/test_dlg_itemeditor.py
@@ -20,6 +20,7 @@ along with this program. If not, see .
"""
import pytest
+import random
from tools import getGuiItem
@@ -88,7 +89,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
@pytest.mark.gui
-def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
+def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, constData):
"""Test the item editor dialog for a novel document.
"""
# Block message box
@@ -96,9 +97,13 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
+ random.seed(42)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
- assert nwGUI.openDocument("0e17daca5f3e1")
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New"
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note"
+
+ assert nwGUI.openDocument("0e17daca5f3e1") is True
# Check that an invalid handle is managed
itemEdit = GuiItemEditor(nwGUI, "whatever")
@@ -111,7 +116,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
# Check Existing Settings
assert itemEdit.editName.text() == "New Scene"
- assert itemEdit.editStatus.currentData() == "New"
+ assert itemEdit.editStatus.currentData() == constData.statusKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
assert itemEdit.editExport.isChecked() is True
@@ -125,7 +130,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Great Scene"
- assert itemEdit.theItem.itemStatus == "Note"
+ assert itemEdit.theItem.itemStatus == constData.statusKeys[1]
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
@@ -141,7 +146,7 @@ def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
@pytest.mark.gui
-def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
+def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData):
"""Test the item editor dialog for a project note.
"""
# Block message box
@@ -149,8 +154,13 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
+ random.seed(42)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New"
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note"
+ assert nwGUI.theProject.importItems.name(constData.importKeys[0]) == "New"
+ assert nwGUI.theProject.importItems.name(constData.importKeys[1]) == "Minor"
# Create Note
nwGUI.treeView.clearSelection()
@@ -166,7 +176,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
# Check Existing Settings
assert itemEdit.editName.text() == "New File"
- assert itemEdit.editStatus.currentData() == "New"
+ assert itemEdit.editStatus.currentData() == constData.importKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True
@@ -178,8 +188,8 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
# Check New Settings
assert itemEdit.theItem.itemName == "New Character"
- assert itemEdit.theItem.itemStatus == "New"
- assert itemEdit.theItem.itemImport == "Minor"
+ assert itemEdit.theItem.itemStatus == constData.statusKeys[0]
+ assert itemEdit.theItem.itemImport == constData.importKeys[1]
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
@@ -191,7 +201,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
@pytest.mark.gui
-def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
+def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, constData):
"""Test the item editor dialog for a folder.
"""
# Block message box
@@ -199,6 +209,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
+ random.seed(42)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
@@ -206,9 +217,12 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
itemEdit = GuiItemEditor(nwGUI, "31489056e0916")
itemEdit.show()
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[0]) == "New"
+ assert nwGUI.theProject.statusItems.name(constData.statusKeys[1]) == "Note"
+
# Check Existing Settings
assert itemEdit.editName.text() == "New Chapter"
- assert itemEdit.editStatus.currentData() == "New"
+ assert itemEdit.editStatus.currentData() == constData.statusKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT
assert itemEdit.editExport.isChecked() is False
@@ -222,7 +236,7 @@ def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Chapter One"
- assert itemEdit.theItem.itemStatus == "Note"
+ assert itemEdit.theItem.itemStatus == constData.statusKeys[1]
assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT
assert itemEdit.theItem.isExported is False
diff --git a/tests/test_dialogs/test_dlg_projload.py b/tests/test_dialogs/test_dlg_projload.py
index 195ca26b..6edb7a0d 100644
--- a/tests/test_dialogs/test_dlg_projload.py
+++ b/tests/test_dialogs/test_dlg_projload.py
@@ -42,7 +42,8 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the load project wizard.
"""
# Block message box
- monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
assert nwGUI.openProject(nwMinimal)
assert nwGUI.closeProject()
diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py
index ec316bea..5a047522 100644
--- a/tests/test_dialogs/test_dlg_projsettings.py
+++ b/tests/test_dialogs/test_dlg_projsettings.py
@@ -19,17 +19,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import pytest
import os
+import pytest
+import random
from shutil import copyfile
from tools import cmpFiles, getGuiItem
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import (
- QDialog, QAction, QMessageBox, QColorDialog, QTreeWidgetItem
-)
+from PyQt5.QtWidgets import QDialog, QAction, QMessageBox, QColorDialog
from novelwriter.dialogs import GuiProjectSettings
@@ -39,7 +38,9 @@ stepDelay = 20
@pytest.mark.gui
-def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir):
+def testDlgProjSettings_Dialog(
+ qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, constData
+):
"""Test the full project settings dialog.
"""
projFile = os.path.join(fncProj, "nwProject.nwx")
@@ -55,6 +56,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
assert getGuiItem("GuiProjectSettings") is None
# Create new project
+ random.seed(42)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
nwGUI.mainConf.backupPath = fncDir
@@ -111,19 +113,9 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
projEdit._tabBox.setCurrentWidget(projEdit.tabStatus)
assert projEdit.tabStatus.colChanged is False
- assert projEdit.tabStatus.getNewList() is None
+ assert projEdit.tabStatus.getNewList() == ([], [])
assert projEdit.tabStatus.listBox.topLevelItemCount() == 4
- # Fake drag'n'drop should change changed status
- projEdit.tabStatus._rowsMoved()
- assert projEdit.tabStatus.colChanged is True
- projEdit.tabStatus.colChanged = False
-
- projEdit.tabStatus.listBox.clearSelection()
- assert projEdit.tabStatus._getSelectedItem() is None
- projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
- assert isinstance(projEdit.tabStatus._getSelectedItem(), QTreeWidgetItem)
-
# Can't delete the first item (it's in use)
projEdit.tabStatus.listBox.clearSelection()
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
@@ -150,11 +142,53 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
qtbot.wait(stepDelay)
assert projEdit.tabStatus.colChanged is True
- assert projEdit.tabStatus.getNewList() == [
- ("New", 100, 100, 100, "New"),
- ("Note", 200, 50, 0, "Note"),
- ("Finished", 50, 200, 0, "Finished"),
- ("Final", 20, 30, 40, None)
+ assert projEdit.tabStatus.getNewList() == (
+ [
+ {
+ "key": constData.statusKeys[0],
+ "name": "New",
+ "cols": (100, 100, 100)
+ }, {
+ "key": constData.statusKeys[1],
+ "name": "Note",
+ "cols": (200, 50, 0)
+ }, {
+ "key": constData.statusKeys[3],
+ "name": "Finished",
+ "cols": (50, 200, 0)
+ }, {
+ "key": None,
+ "name": "Final",
+ "cols": (20, 30, 40)
+ }
+ ], [
+ constData.statusKeys[2] # Deleted item
+ ]
+ )
+
+ # Move items
+ projEdit.tabStatus.listBox.clearSelection()
+ projEdit.tabStatus._moveItem(1)
+ assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
+ constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None
+ ]
+
+ projEdit.tabStatus.listBox.clearSelection()
+ projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
+ projEdit.tabStatus._moveItem(-1)
+ assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
+ constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None
+ ]
+
+ projEdit.tabStatus.listBox.clearSelection()
+ projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True)
+ projEdit.tabStatus._moveItem(-1)
+ assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
+ constData.statusKeys[0], constData.statusKeys[1], None, constData.statusKeys[3]
+ ]
+ projEdit.tabStatus._moveItem(1)
+ assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
+ constData.statusKeys[0], constData.statusKeys[1], constData.statusKeys[3], None
]
# Importance Tab
diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py
index 3ca5a0ee..d9993d4d 100644
--- a/tests/test_dialogs/test_dlg_wordlist.py
+++ b/tests/test_dialogs/test_dlg_wordlist.py
@@ -40,6 +40,7 @@ stepDelay = 20
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal):
"""test the word list editor.
"""
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None)
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 8057ca41..eaa1d2ba 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -43,6 +43,7 @@ def testGuiEditor_Init(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
# Open project
assert nwGUI.openProject(nwMinimal)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 34cfaffa..fb36b3e2 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -19,8 +19,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import pytest
import os
+import random
+import pytest
from shutil import copyfile
from tools import cmpFiles
@@ -139,6 +140,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
# Create new, save, close project
+ random.seed(42)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.saveProject()
diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py
index 6adac08e..da7fc414 100644
--- a/tests/test_gui/test_gui_theme.py
+++ b/tests/test_gui/test_gui_theme.py
@@ -35,6 +35,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
"""Test the theme and icon classes.
"""
# Block message box
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)