From 9c9408d78a868c94c2d599dbe5c118e4a4a19f54 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:22:41 +0200 Subject: [PATCH 01/17] Changing status or importance flags should still update hidden flags --- novelwriter/core/project.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ff2c2774..04e5242e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -1071,9 +1071,8 @@ class NWProject(): """ 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]) + if nwItem.itemStatus in replaceMap: + nwItem.setStatus(replaceMap[nwItem.itemStatus]) self.setProjectChanged(True) return True @@ -1083,9 +1082,8 @@ class NWProject(): """ 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]) + if nwItem.itemImport in replaceMap: + nwItem.setImport(replaceMap[nwItem.itemImport]) self.setProjectChanged(True) return True From 7298e1f438b3110a81d2f31e4d92a6be827ab910 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 22:07:20 +0200 Subject: [PATCH 02/17] Add two new utility functions to the common module --- novelwriter/common.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/novelwriter/common.py b/novelwriter/common.py index 8e17a7ee..30ac0f2e 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -184,6 +184,12 @@ def checkIntRange(value, first, last, default): return default +def getMinMax(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. From 40e7055f167c5f5f9442636e52a73c31be358807 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 22:08:14 +0200 Subject: [PATCH 03/17] Rewrite most of the NWStatus class --- novelwriter/core/item.py | 12 +- novelwriter/core/project.py | 24 ++-- novelwriter/core/status.py | 253 ++++++++++++++++++++---------------- 3 files changed, 162 insertions(+), 127 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 993b183c..d56d60f2 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -280,11 +280,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): @@ -383,14 +383,14 @@ class NWItem(): """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(theStatus) return def setImport(self, theImport): """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(theImport) return def setExpanded(self, expState): diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 04e5242e..ee6f65ba 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -218,16 +218,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("s") + 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("i") + 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 @@ -1205,9 +1205,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): diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 2ca8ed53..0f2f1aea 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -23,6 +23,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 +31,158 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPixmap, QColor -from novelwriter.common import checkInt +from novelwriter.common import checkInt, getMinMax, simplified logger = logging.getLogger(__name__) class NWStatus(): - def __init__(self): + def __init__(self, type): + + self._type = str(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) 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): + """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) - return True + pixmap = QPixmap(self._iconSize, self._iconSize) + pixmap.fill(QColor(*cols)) - def checkEntry(self, theStatus): - """Check if a status value is valid, and returns the safe - reference to be used internally. + name = simplified(name) + 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 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 cols(self, key): + """Return the colours associated with a given key. + """ + 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) + + 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 + + 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 def setNewEntries(self, newList): """Update the list of entries after they have been modified by the GUI tool. """ - replaceMap = {} - - if newList is not None: - self._theLabels = [] - self._theColours = [] - self._theCounts = [] - self._theIcons = [] - self._theMap = {} - self._theLength = 0 - self._theIndex = 0 - - for nName, nR, nG, nB, oName in newList: - self.addEntry(nName, (nR, nG, nB)) - if nName != oName and oName is not None: - replaceMap[oName] = nName - - return replaceMap + return {} 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, + "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]) + name = xChild.text.strip() + key = xChild.attrib.get("key", None) + cR = getMinMax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) + cG = getMinMax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) + cB = getMinMax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) + self.write(key, name, (cR, cG, cB)) return True @@ -165,39 +190,49 @@ 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._type}{random.randint(0, 0xffffff):06x}" + if key in self._store: + key = self._newKey() + return key + + def _isKey(self, key): + """Check if a string is a key or not. + """ + if not isinstance(key, str): + return False + if len(key) != 7: + return False + if key[0] != self._type: + return False + for c in key[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 __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 From ddd8b6081647d3ca29776b29710127cbede18ef0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:13:39 +0200 Subject: [PATCH 04/17] Complete the needed GUI class changes for the new NWStatus class --- novelwriter/common.py | 2 +- novelwriter/core/project.py | 45 ++++++++--- novelwriter/core/status.py | 44 +++++++---- novelwriter/dialogs/projsettings.py | 118 +++++++++++++++------------- novelwriter/gui/itemdetails.py | 5 ++ novelwriter/guimain.py | 4 +- 6 files changed, 135 insertions(+), 83 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 30ac0f2e..69f6bc05 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -184,7 +184,7 @@ def checkIntRange(value, first, last, default): return default -def getMinMax(value, minVal, maxVal): +def minmax(value, minVal, maxVal): """Make sure an integer is between min and max value (inclusive). """ return min(maxVal, max(minVal, value)) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ee6f65ba..d17cffc3 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -687,6 +687,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") @@ -1018,7 +1020,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. @@ -1065,26 +1068,46 @@ class NWProject(): self.setProjectChanged(True) return True - def setStatusColours(self, newCols): + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. Also iterate through the project and replace keys that have been renamed. """ - replaceMap = self.statusItems.setNewEntries(newCols) - for nwItem in self.projTree: - if nwItem.itemStatus in replaceMap: - nwItem.setStatus(replaceMap[nwItem.itemStatus]) + if not (newCols or delCols): + return False + + for entry in newCols: + key = entry.get("key", None) + name = entry.get("name", "") + cols = entry.get("cols", (100, 100, 100)) + if name: + self.statusItems.write(key, name, cols) + + for key in delCols: + self.statusItems.remove(key) + self.setProjectChanged(True) + return True - def setImportColours(self, newCols): + def setImportColours(self, newCols, delCols): """Update the list of note file importance flags. Also iterate through the project and replace keys that have been renamed. """ - replaceMap = self.importItems.setNewEntries(newCols) - for nwItem in self.projTree: - if nwItem.itemImport in replaceMap: - nwItem.setImport(replaceMap[nwItem.itemImport]) + if not (newCols or delCols): + return False + + for entry in newCols: + key = entry.get("key", None) + name = entry.get("name", "") + cols = entry.get("cols", (100, 100, 100)) + if name: + self.importItems.write(key, name, cols) + + for key in delCols: + self.importItems.remove(key) + self.setProjectChanged(True) + return True def setAutoReplace(self, autoReplace): diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 0f2f1aea..4aee93b2 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 @@ -31,7 +32,7 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPixmap, QColor -from novelwriter.common import checkInt, getMinMax, simplified +from novelwriter.common import checkInt, minmax, simplified logger = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class NWStatus(): return - def write(self, key, name, cols): + def write(self, key, name, cols, count=None): """Add or update a status entry. If the key is invalid, a new key is generated. """ @@ -67,7 +68,8 @@ class NWStatus(): pixmap.fill(QColor(*cols)) name = simplified(name) - count = self._store[key]["count"] if key in self._store else 0 + if count is None: + count = self._store[key]["count"] if key in self._store else 0 self._store[key] = { "name": name, @@ -82,6 +84,20 @@ class NWStatus(): 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] + + return True + def check(self, value): """Check the key against the stored status names. """ @@ -134,12 +150,6 @@ class NWStatus(): else: return self._defaultIcon - def setNewEntries(self, newList): - """Update the list of entries after they have been modified by - the GUI tool. - """ - return {} - def resetCounts(self): """Clear the counts of references to the status entries. """ @@ -161,6 +171,7 @@ class NWStatus(): for key, data in self._store.items(): xSub = etree.SubElement(xParent, "entry", attrib={ "key": key, + "count": str(data["count"]), "red": str(data["cols"][0]), "green": str(data["cols"][1]), "blue": str(data["cols"][2]), @@ -177,12 +188,13 @@ class NWStatus(): self._default = None for xChild in xParent: - name = xChild.text.strip() - key = xChild.attrib.get("key", None) - cR = getMinMax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) - cG = getMinMax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) - cB = getMinMax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) - self.write(key, name, (cR, cG, cB)) + 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 diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index f6e4b611..260f8b10 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, data in self.theStatus.items(): + self._addItem(key, data["name"], data["cols"], data["count"]) # List Controls # ============= @@ -349,12 +358,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 +381,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"), (0, 0, 0), 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 +402,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,36 +417,33 @@ 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 _selectedItem(self): """Extract the info of a selected item and populate the settings @@ -442,13 +451,14 @@ class GuiProjectEditStatus(QWidget): """ 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)) + 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() 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/guimain.py b/novelwriter/guimain.py index 9db27659..2ffffe45 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -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 From ac904ee9fc36935578ed3ec1f58235d5e04061e8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 5 Apr 2022 23:15:18 +0200 Subject: [PATCH 05/17] Update sample project file --- sample/nwProject.nwx | 76 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) 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!
From e98e942f198b957b27121371ba8d7eba0856be4f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 14:59:25 +0200 Subject: [PATCH 06/17] Replace key generator in the NWStatus class --- novelwriter/core/project.py | 5 +++-- novelwriter/core/status.py | 26 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index d17cffc3..b00b73cf 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -218,12 +218,12 @@ class NWProject(): } self.spellCheck = False self.autoOutline = True - self.statusItems = NWStatus("s") + 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("i") + 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)) @@ -267,6 +267,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 diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 4aee93b2..a3792764 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -39,9 +39,12 @@ logger = logging.getLogger(__name__) class NWStatus(): + STATUS = 1 + IMPORT = 2 + def __init__(self, type): - self._type = str(type) + self._type = type self._store = {} self._reverse = {} self._default = None @@ -51,6 +54,13 @@ class NWStatus(): 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 write(self, key, name, cols, count=None): @@ -209,21 +219,21 @@ class NWStatus(): flags. The Python recursion limit is given the job to handle the extreme case and will cause an app crash. """ - key = f"{self._type}{random.randint(0, 0xffffff):06x}" + key = f"{self._prefix}{random.getrandbits(24):06x}" if key in self._store: key = self._newKey() return key - def _isKey(self, key): - """Check if a string is a key or not. + def _isKey(self, value): + """Check if a value is a key or not. """ - if not isinstance(key, str): + if not isinstance(value, str): return False - if len(key) != 7: + if len(value) != 7: return False - if key[0] != self._type: + if value[0] != self._prefix: return False - for c in key[1:]: + for c in value[1:]: if c not in "0123456789abcdef": return False return True From cf7e1da96aad29d52ccafc7eb6e5f19bda57a22a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 14:59:59 +0200 Subject: [PATCH 07/17] Fix display text issues in varuious GUI elements --- novelwriter/dialogs/itemeditor.py | 30 ++++++++++++++++------------- novelwriter/dialogs/projsettings.py | 4 ++-- novelwriter/gui/outlinedetails.py | 4 +++- 3 files changed, 22 insertions(+), 16 deletions(-) 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 260f8b10..9599cc92 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -294,8 +294,8 @@ class GuiProjectEditStatus(QWidget): self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for key, data in self.theStatus.items(): - self._addItem(key, data["name"], data["cols"], data["count"]) + for key, entry in self.theStatus.items(): + self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls # ============= 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) From fedb064b1f6b2ec62414aca9da7b7eda10c03805 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 15:01:55 +0200 Subject: [PATCH 08/17] Fix other tests --- tests/conftest.py | 19 +++- tests/lipsum/nwProject.nwx | 64 +++++------ tests/minimal/nwProject.nwx | 38 +++---- .../coreProject_NewCustomA_nwProject.nwx | 64 +++++------ .../coreProject_NewCustomB_nwProject.nwx | 46 ++++---- .../coreProject_NewFile_nwProject.nwx | 38 +++---- .../coreProject_NewMinimal_nwProject.nwx | 34 +++--- .../coreProject_NewRoot_nwProject.nwx | 42 +++---- .../guiEditor_Main_Final_nwProject.nwx | 40 +++---- .../guiEditor_Main_Initial_nwProject.nwx | 34 +++--- .../guiProjSettings_Dialog_nwProject.nwx | 34 +++--- tests/test_base/test_base_common.py | 27 ++++- tests/test_core/test_core_item.py | 77 ++++++------- tests/test_core/test_core_project.py | 107 ++++++++++-------- tests/test_dialogs/test_dlg_itemeditor.py | 36 ++++-- tests/test_dialogs/test_dlg_projload.py | 3 +- tests/test_dialogs/test_dlg_projsettings.py | 39 +++++-- tests/test_dialogs/test_dlg_wordlist.py | 1 + tests/test_gui/test_gui_doceditor.py | 1 + tests/test_gui/test_gui_guimain.py | 4 +- tests/test_gui/test_gui_theme.py | 1 + 21 files changed, 412 insertions(+), 337 deletions(-) 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 55a582ad..8ffb64fc 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 6f45815d..9824e2bb 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 10398c21..65d56307 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 2bb2df31..baab7a17 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 d4ffb126..978f5855 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 20fa4d71..fc2ad735 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 22d51fd7..94fdbeea 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -27,66 +27,66 @@
- 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 - 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 685095bb..3432d06b 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 a42552cf..e6e80636 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 c6652f72..f41ac5c8 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project 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 61c67987..eb1425b3 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) @@ -77,50 +79,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) @@ -354,9 +338,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") @@ -370,7 +355,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) @@ -385,9 +370,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): b'' b'' - b'A Name' + b'A Name
' b'
' - ) + ) % bytes(constData.importKeys[3], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -403,6 +388,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 # ====== @@ -414,7 +401,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) @@ -429,10 +416,10 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( b'' b'A Name' + b'class="NOVEL">A Name' b'' b'' - ) + ) % bytes(constData.statusKeys[1], encoding="utf8") # Unpack theItem = NWItem(theProject) @@ -449,6 +436,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 311fca8c..b98b05f4 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) @@ -770,9 +776,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" @@ -827,55 +834,55 @@ 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 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" + # # 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] + # # 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 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..f2aa8319 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.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 tools import cmpFiles, getGuiItem @@ -39,7 +40,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 +58,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,7 +115,7 @@ 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 @@ -150,12 +154,29 @@ 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 + ] + ) # 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) From d7a8cb1537f70929b360a1b2b06355b19001b4d1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 15:38:13 +0200 Subject: [PATCH 09/17] Rewrite NWStatus class tests --- novelwriter/core/status.py | 10 + tests/test_core/test_core_status.py | 344 +++++++++++++++++++++------- 2 files changed, 268 insertions(+), 86 deletions(-) diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index a3792764..4e9317f2 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -106,6 +106,13 @@ class NWStatus(): 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 check(self, value): @@ -242,6 +249,9 @@ class NWStatus(): # Iterator Bits ## + def __len__(self): + return len(self._store) + def __getitem__(self, key): return self._store[key] diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index a7a2d55e..611f8327 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,268 @@ 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 + + # 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 +300,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 From c5f92edaa845d547b7b121fd1fd017a9eee8aceb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:27:46 +0200 Subject: [PATCH 10/17] Update the NWProject class test for status and import labels --- tests/test_core/test_core_project.py | 197 ++++++++++++++++++--------- 1 file changed, 131 insertions(+), 66 deletions(-) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index b98b05f4..d4213442 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -686,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) @@ -703,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 ") @@ -750,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 @@ -819,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) @@ -834,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 @@ -897,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 From 048e5faa1ff6c44be7d85ad57bbd5b807c9de1e4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:44:39 +0200 Subject: [PATCH 11/17] Also simplify item labels, due to XML limitations --- novelwriter/core/item.py | 4 ++-- tests/test_core/test_core_item.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index d56d60f2..2672a196 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 @@ -305,7 +305,7 @@ class NWItem(): """Set the item name. """ if isinstance(theName, str): - self._name = theName.strip() + self._name = simplified(theName) else: self._name = "" return diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index eb1425b3..3adda4de 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -44,6 +44,8 @@ def testCoreItem_Setters(mockGUI, constData): 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 == "" From 72c5a9ee92fe866ffafa39893f28d45e7ae508ea Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:47:57 +0200 Subject: [PATCH 12/17] Rename some internal variables in the NWItem class --- novelwriter/core/item.py | 102 +++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 2672a196..f9c1921d 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -301,142 +301,142 @@ class NWItem(): # Set Item Values ## - def setName(self, theName): + def setName(self, name): """Set the item name. """ - if isinstance(theName, str): - self._name = simplified(theName) + 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 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.check(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.check(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): From 2f547033cd9e7f61fa6bf67deb506610fb8a0ea6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:00:08 +0200 Subject: [PATCH 13/17] Simplify all other strings in main project class --- novelwriter/core/project.py | 30 +++++++++++-------- .../guiProjSettings_Dialog_nwProject.nwx | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index b00b73cf..a8a9cce3 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 @@ -534,14 +534,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": @@ -956,14 +958,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 @@ -975,7 +977,7 @@ class NWProject(): self.bookAuthors = [] for bookAuthor in bookAuthors.splitlines(): - bookAuthor = bookAuthor.strip() + bookAuthor = simplified(bookAuthor) if bookAuthor == "": continue self.bookAuthors.append(bookAuthor) @@ -1114,7 +1116,9 @@ class NWProject(): 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 @@ -1123,7 +1127,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): diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index f41ac5c8..e894d5bf 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% From 17c56440f22881fa3beaca80c60db7ea39ec6159 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:28:28 +0200 Subject: [PATCH 14/17] Automatic indexing of un-indexed files (#1039) * Downgrade rebuild index dialog from warning to info * Don't flag index as broken if file is missing, just add it again * Fix log warning * Drop the return value in the index checker --- novelwriter/core/index.py | 18 ++++++++++-------- novelwriter/guimain.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b05fa952..39bdf0fb 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -681,16 +681,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/guimain.py b/novelwriter/guimain.py index 2ffffe45..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 From fd2248de171532cc5c84233135ca1497806600ec Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 17:44:54 +0200 Subject: [PATCH 15/17] Add up and down button icons --- .../assets/icons/typicons_dark/icons.conf | 2 ++ .../icons/typicons_dark/typ_chevron-down.svg | 31 +++++++++++++++++++ .../icons/typicons_dark/typ_chevron-up.svg | 31 +++++++++++++++++++ .../assets/icons/typicons_light/icons.conf | 2 ++ .../icons/typicons_light/typ_chevron-down.svg | 31 +++++++++++++++++++ .../icons/typicons_light/typ_chevron-up.svg | 31 +++++++++++++++++++ novelwriter/gui/theme.py | 2 +- 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg create mode 100644 novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_chevron-down.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_chevron-up.svg 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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + 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", From 6aa13e318bc82af5e62a942b6fcc7468210557b6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 18:17:01 +0200 Subject: [PATCH 16/17] Add sorting capability to status and importance labels --- novelwriter/core/project.py | 62 ++++++++++++---------------- novelwriter/core/status.py | 22 ++++++++++ novelwriter/dialogs/projsettings.py | 64 ++++++++++++++++++++--------- 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a8a9cce3..9623520e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -1072,46 +1072,14 @@ class NWProject(): return True def setStatusColours(self, newCols, delCols): - """Update the list of novel file status flags. Also iterate - through the project and replace keys that have been renamed. + """Update the list of novel file status flags. """ - if not (newCols or delCols): - return False - - for entry in newCols: - key = entry.get("key", None) - name = entry.get("name", "") - cols = entry.get("cols", (100, 100, 100)) - if name: - self.statusItems.write(key, name, cols) - - for key in delCols: - self.statusItems.remove(key) - - self.setProjectChanged(True) - - return True + return self._setStatusImport(newCols, delCols, self.statusItems) def setImportColours(self, newCols, delCols): - """Update the list of note file importance flags. Also iterate - through the project and replace keys that have been renamed. + """Update the list of note file importance flags. """ - if not (newCols or delCols): - return False - - for entry in newCols: - key = entry.get("key", None) - name = entry.get("name", "") - cols = entry.get("cols", (100, 100, 100)) - if name: - self.importItems.write(key, name, cols) - - for key in delCols: - self.importItems.remove(key) - - self.setProjectChanged(True) - - return True + return self._setStatusImport(newCols, delCols, self.importItems) def setAutoReplace(self, autoReplace): """Update the auto-replace dictionary. @@ -1251,6 +1219,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 4e9317f2..9fcfbb88 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -167,6 +167,28 @@ class NWStatus(): else: return self._defaultIcon + 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 True + + 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. """ diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 9599cc92..a0987418 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -306,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 # ========= @@ -315,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) @@ -329,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() @@ -390,7 +398,7 @@ class GuiProjectEditStatus(QWidget): def _newItem(self): """Create a new status item. """ - newItem = self._addItem(None, self.tr("New Item"), (0, 0, 0), 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 @@ -445,23 +453,47 @@ class GuiProjectEditStatus(QWidget): 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 False + + 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: - cols = selItem.data(self.COL_LABEL, self.COL_ROLE) - name = selItem.text(self.COL_LABEL) + if selItem is None: + return - 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() + 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 @@ -477,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. """ From 3ed74f0b02b94aee34846a881ad39974fca0af70 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 18:35:26 +0200 Subject: [PATCH 17/17] Add test coverage of status and importance reordering --- novelwriter/core/status.py | 2 +- novelwriter/dialogs/projsettings.py | 2 +- .../guiProjSettings_Dialog_nwProject.nwx | 4 +- tests/test_core/test_core_status.py | 32 +++++++++++++++ tests/test_dialogs/test_dlg_projsettings.py | 39 ++++++++++++------- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 9fcfbb88..4bade5e7 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -175,7 +175,7 @@ class NWStatus(): return False if order == list(self._store.keys()): - return True + return False store = {} for key in order: diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index a0987418..2a2bb969 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -464,7 +464,7 @@ class GuiProjectEditStatus(QWidget): nChild = self.listBox.topLevelItemCount() nIndex = tIndex + step if nIndex < 0 or nIndex >= nChild: - return False + return cItem = self.listBox.takeTopLevelItem(tIndex) self.listBox.insertTopLevelItem(nIndex, cItem) diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index e894d5bf..1b3c818a 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -42,7 +42,7 @@ New Minor Major - Final + Final diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 611f8327..e1d224d8 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -229,6 +229,38 @@ def testCoreStatus_Entries(constData): 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 # ======= diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index f2aa8319..5a047522 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -28,9 +28,7 @@ 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 @@ -118,16 +116,6 @@ def testDlgProjSettings_Dialog( 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) @@ -178,6 +166,31 @@ def testDlgProjSettings_Dialog( ] ) + # 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 # ==============