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