Merge branch 'main' into multi_root

This commit is contained in:
Veronica Berglyd Olsen
2022-04-16 18:49:46 +02:00
40 changed files with 1461 additions and 801 deletions
+10 -8
View File
@@ -679,16 +679,18 @@ class NWIndex():
logException()
self._indexBroken = True
# Check that project files are indexed
for fHandle in self.theProject.projFiles:
if fHandle not in self._fileMeta:
self._indexBroken = True
break
logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
if self._indexBroken:
self.clearIndex()
logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
return
# If the index was ok, we check that project files are indexed
for fHandle in self.theProject.projFiles:
if fHandle not in self._fileMeta:
logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle)
logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000)
return
+60 -60
View File
@@ -29,7 +29,7 @@ from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, nwLists, trConst
@@ -287,11 +287,11 @@ class NWItem():
the current item based on its class.
"""
if self._class in nwLists.CLS_NOVEL:
stName = self.theProject.statusItems.checkEntry(self._status)
stIcon = self.theProject.statusItems.getIcon(stName)
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
else:
stName = self.theProject.importItems.checkEntry(self._import)
stIcon = self.theProject.importItems.getIcon(stName)
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
def setImportStatus(self, theLabel):
@@ -308,153 +308,153 @@ class NWItem():
# Set Item Values
##
def setName(self, theName):
def setName(self, name):
"""Set the item name.
"""
if isinstance(theName, str):
self._name = theName.strip()
if isinstance(name, str):
self._name = simplified(name)
else:
self._name = ""
return
def setHandle(self, theHandle):
def setHandle(self, tHandle):
"""Set the item handle, and ensure it is valid.
"""
if isHandle(theHandle):
self._handle = theHandle
if isHandle(tHandle):
self._handle = tHandle
else:
self._handle = None
return
def setParent(self, theParent):
def setParent(self, pHandle):
"""Set the parent handle, and ensure it is valid.
"""
if theParent is None:
if pHandle is None:
self._parent = None
elif isHandle(theParent):
self._parent = theParent
elif isHandle(pHandle):
self._parent = pHandle
else:
self._parent = None
return
def setRoot(self, theRoot):
def setRoot(self, rHandle):
"""Set the root handle, and ensure it is valid.
"""
if theRoot is None:
if rHandle is None:
self._root = None
elif isHandle(theRoot):
self._root = theRoot
elif isHandle(rHandle):
self._root = rHandle
else:
self._root = None
return
def setOrder(self, theOrder):
def setOrder(self, order):
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at
the moment.
"""
self._order = checkInt(theOrder, 0)
self._order = checkInt(order, 0)
return
def setType(self, theType):
def setType(self, itemType):
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
if isinstance(theType, nwItemType):
self._type = theType
elif isItemType(theType):
self._type = nwItemType[theType]
if isinstance(itemType, nwItemType):
self._type = itemType
elif isItemType(itemType):
self._type = nwItemType[itemType]
else:
logger.error("Unrecognised item type '%s'", theType)
logger.error("Unrecognised item type '%s'", itemType)
self._type = nwItemType.NO_TYPE
return
def setClass(self, theClass):
def setClass(self, itemClass):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
if isinstance(theClass, nwItemClass):
self._class = theClass
elif isItemClass(theClass):
self._class = nwItemClass[theClass]
if isinstance(itemClass, nwItemClass):
self._class = itemClass
elif isItemClass(itemClass):
self._class = nwItemClass[itemClass]
else:
logger.error("Unrecognised item class '%s'", theClass)
logger.error("Unrecognised item class '%s'", itemClass)
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, theLayout):
def setLayout(self, itemLayout):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
if isinstance(theLayout, nwItemLayout):
self._layout = theLayout
elif isItemLayout(theLayout):
self._layout = nwItemLayout[theLayout]
elif theLayout in nwLists.DEP_LAYOUT:
if isinstance(itemLayout, nwItemLayout):
self._layout = itemLayout
elif isItemLayout(itemLayout):
self._layout = nwItemLayout[itemLayout]
elif itemLayout in nwLists.DEP_LAYOUT:
self._layout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", theLayout)
logger.error("Unrecognised item layout '%s'", itemLayout)
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, theStatus):
def setStatus(self, itemStatus):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self.theProject.statusItems.checkEntry(theStatus)
self._status = self.theProject.statusItems.check(itemStatus)
return
def setImport(self, theImport):
def setImport(self, itemImport):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self.theProject.importItems.checkEntry(theImport)
self._import = self.theProject.importItems.check(itemImport)
return
def setExpanded(self, expState):
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
if isinstance(expState, str):
self._expanded = (expState == str(True))
if isinstance(state, str):
self._expanded = (state == str(True))
else:
self._expanded = (expState is True)
self._expanded = (state is True)
return
def setExported(self, expState):
def setExported(self, state):
"""Set the export flag.
"""
if isinstance(expState, str):
self._exported = (expState == str(True))
if isinstance(state, str):
self._exported = (state == str(True))
else:
self._exported = (expState is True)
self._exported = (state is True)
return
##
# Set Document Meta Data
##
def setCharCount(self, theCount):
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
self._charCount = max(0, checkInt(theCount, 0))
self._charCount = max(0, checkInt(count, 0))
return
def setWordCount(self, theCount):
def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer.
"""
self._wordCount = max(0, checkInt(theCount, 0))
self._wordCount = max(0, checkInt(count, 0))
return
def setParaCount(self, theCount):
def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer.
"""
self._paraCount = max(0, checkInt(theCount, 0))
self._paraCount = max(0, checkInt(count, 0))
return
def setCursorPos(self, thePosition):
def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer.
"""
self._cursorPos = max(0, checkInt(thePosition, 0))
self._cursorPos = max(0, checkInt(position, 0))
return
def saveInitialCount(self):
+63 -45
View File
@@ -44,7 +44,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException
from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt
makeFileNameSafe, hexToInt, simplified
)
from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels
@@ -217,16 +217,16 @@ class NWProject():
}
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
self.statusItems.addEntry(self.tr("New"), (100, 100, 100))
self.statusItems.addEntry(self.tr("Note"), (200, 50, 0))
self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0))
self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus()
self.importItems.addEntry(self.tr("New"), (100, 100, 100))
self.importItems.addEntry(self.tr("Minor"), (200, 50, 0))
self.importItems.addEntry(self.tr("Major"), (200, 150, 0))
self.importItems.addEntry(self.tr("Main"), (50, 200, 0))
self.statusItems = NWStatus(NWStatus.STATUS)
self.statusItems.write(None, self.tr("New"), (100, 100, 100))
self.statusItems.write(None, self.tr("Note"), (200, 50, 0))
self.statusItems.write(None, self.tr("Draft"), (200, 150, 0))
self.statusItems.write(None, self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus(NWStatus.IMPORT)
self.importItems.write(None, self.tr("New"), (100, 100, 100))
self.importItems.write(None, self.tr("Minor"), (200, 50, 0))
self.importItems.write(None, self.tr("Major"), (200, 150, 0))
self.importItems.write(None, self.tr("Main"), (50, 200, 0))
self.lastEdited = None
self.lastViewed = None
self.lastWCount = 0
@@ -266,6 +266,7 @@ class NWProject():
logger.error("No project path set for the new project")
return False
self.clearProject()
if not self.setProjectPath(projPath, newProject=True):
return False
@@ -532,14 +533,16 @@ class NWProject():
if xItem.text is None:
continue
if xItem.tag == "name":
logger.verbose("Working Title: '%s'", xItem.text)
self.projName = xItem.text
self.projName = checkString(simplified(xItem.text), "")
logger.verbose("Working Title: '%s'", self.projName)
elif xItem.tag == "title":
logger.verbose("Title is '%s'", xItem.text)
self.bookTitle = xItem.text
self.bookTitle = checkString(simplified(xItem.text), "")
logger.verbose("Title is '%s'", self.bookTitle)
elif xItem.tag == "author":
logger.verbose("Author: '%s'", xItem.text)
self.bookAuthors.append(xItem.text)
author = checkString(simplified(xItem.text), "")
if author:
self.bookAuthors.append(author)
logger.verbose("Author: '%s'", author)
elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount":
@@ -693,6 +696,8 @@ class NWProject():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
# Save Status/Importance
self.countStatus()
xStatus = etree.SubElement(xSettings, "status")
self.statusItems.packXML(xStatus)
xStatus = etree.SubElement(xSettings, "importance")
@@ -959,14 +964,14 @@ class NWProject():
"""Set the project name (working title), This is the the title
used for backup files etc.
"""
self.projName = projName.strip()
self.projName = simplified(projName)
self.setProjectChanged(True)
return True
def setBookTitle(self, bookTitle):
"""Set the book title, that is, the title to include in exports.
"""
self.bookTitle = bookTitle.strip()
self.bookTitle = simplified(bookTitle)
self.setProjectChanged(True)
return True
@@ -978,7 +983,7 @@ class NWProject():
self.bookAuthors = []
for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip()
bookAuthor = simplified(bookAuthor)
if bookAuthor == "":
continue
self.bookAuthors.append(bookAuthor)
@@ -1024,7 +1029,8 @@ class NWProject():
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
return True
return True
return False
def setProjectLang(self, theLang):
"""Set the project-specific language.
@@ -1071,34 +1077,22 @@ class NWProject():
self.setProjectChanged(True)
return True
def setStatusColours(self, newCols):
"""Update the list of novel file status flags. Also iterate
through the project and replace keys that have been renamed.
def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags.
"""
replaceMap = self.statusItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
return self._setStatusImport(newCols, delCols, self.statusItems)
def setImportColours(self, newCols):
"""Update the list of note file importance flags. Also iterate
through the project and replace keys that have been renamed.
def setImportColours(self, newCols, delCols):
"""Update the list of note file importance flags.
"""
replaceMap = self.importItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass not in nwLists.CLS_NOVEL:
if nwItem.itemImport in replaceMap:
nwItem.setImport(replaceMap[nwItem.itemImport])
self.setProjectChanged(True)
return True
return self._setStatusImport(newCols, delCols, self.importItems)
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary.
"""
self.autoReplace = autoReplace
self.autoReplace = {}
for key, entry in autoReplace.items():
self.autoReplace[key] = simplified(entry)
self.setProjectChanged(True)
return True
@@ -1107,7 +1101,9 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
self.titleFormat[valKey] = checkString(
simplified(valEntry), self.titleFormat[valKey]
)
return True
def setProjectChanged(self, bValue):
@@ -1213,9 +1209,9 @@ class NWProject():
self.importItems.resetCounts()
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
self.statusItems.countEntry(nwItem.itemStatus)
self.statusItems.increment(nwItem.itemStatus)
else:
self.importItems.countEntry(nwItem.itemImport)
self.importItems.increment(nwItem.itemImport)
return
def localLookup(self, theWord):
@@ -1229,6 +1225,28 @@ class NWProject():
# Internal Functions
##
def _setStatusImport(self, new, delete, target):
"""Update the list of novel file status or importance flags, and
delete those that have been requested deleted.
"""
if not (new or delete):
return False
order = []
for entry in new:
key = entry.get("key", None)
name = entry.get("name", "")
cols = entry.get("cols", (100, 100, 100))
if name:
order.append(target.write(key, name, cols))
for key in delete:
target.remove(key)
target.reorder(order)
return True
def _loadProjectLocalisation(self):
"""Load the language data for the current project language.
"""
+198 -109
View File
@@ -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 20182022, Veronica Berglyd Olsen
@@ -23,6 +24,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import random
import logging
import novelwriter
@@ -30,134 +32,208 @@ from lxml import etree
from PyQt5.QtGui import QIcon, QPixmap, QColor
from novelwriter.common import checkInt
from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
def __init__(self):
STATUS = 1
IMPORT = 2
def __init__(self, type):
self._type = type
self._store = {}
self._reverse = {}
self._default = None
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
self._iconSize = novelwriter.CONFIG.pxInt(32)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(100, 100, 100))
self._defaultIcon = QIcon(pixmap)
if self._type == self.STATUS:
self._prefix = "s"
elif self._type == self.IMPORT:
self._prefix = "i"
else:
raise Exception("This is a bug!")
return
def addEntry(self, theLabel, theColours):
"""Add a status entry to the status object, but ensure it isn't
a duplicate.
def write(self, key, name, cols, count=None):
"""Add or update a status entry. If the key is invalid, a new
key is generated.
"""
theLabel = theLabel.strip()
if self._getIndex(theLabel) is None:
theIcon = QPixmap(self._iconSize, self._iconSize)
theIcon.fill(QColor(*theColours))
self._theIcons.append(QIcon(theIcon))
self._theLabels.append(theLabel)
self._theColours.append(theColours)
self._theCounts.append(0)
self._theMap[theLabel] = self._theLength
self._theLength += 1
if not self._isKey(key):
key = self._newKey()
if not isinstance(cols, tuple):
cols = (100, 100, 100)
if len(cols) != 3:
cols = (100, 100, 100)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(*cols))
name = simplified(name)
if count is None:
count = self._store[key]["count"] if key in self._store else 0
self._store[key] = {
"name": name,
"icon": QIcon(pixmap),
"cols": cols,
"count": count,
}
self._reverse[name] = key
if self._default is None:
self._default = key
return key
def remove(self, key):
"""Remove an entry in the list, but not if the count is larger
than 0.
"""
if key not in self._store:
return False
if self._store[key]["count"] > 0:
return False
del self._reverse[self._store[key]["name"]]
del self._store[key]
keys = list(self._store.keys())
if key == self._default:
if len(keys) > 0:
self._default = keys[0]
else:
self._default = None
return True
def checkEntry(self, theStatus):
"""Check if a status value is valid, and returns the safe
reference to be used internally.
def check(self, value):
"""Check the key against the stored status names.
"""
if isinstance(theStatus, str):
if self._getIndex(theStatus) is not None:
return theStatus.strip()
return self._theLabels[0]
if self._isKey(value) and value in self._store:
return value
elif value in self._reverse:
return self._reverse[value]
elif self._default is not None:
return self._default
else:
return ""
def getIcon(self, theLabel):
"""Return the icon for the given status item.
def name(self, key):
"""Return the name associated with a given key.
"""
theIndex = self._getIndex(theLabel)
if theIndex is not None:
return self._theIcons[theIndex]
return QIcon()
if key in self._store:
return self._store[key]["name"]
elif self._default is not None:
return self._store[self._default]["name"]
else:
return ""
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
the GUI tool.
def cols(self, key):
"""Return the colours associated with a given key.
"""
replaceMap = {}
if key in self._store:
return self._store[key]["cols"]
elif self._default is not None:
return self._store[self._default]["cols"]
else:
return (100, 100, 100)
if newList is not None:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
def count(self, key):
"""Return the count associated with a given key.
"""
if key in self._store:
return self._store[key]["count"]
elif self._default is not None:
return self._store[self._default]["count"]
else:
return 0
for nName, nR, nG, nB, oName in newList:
self.addEntry(nName, (nR, nG, nB))
if nName != oName and oName is not None:
replaceMap[oName] = nName
def icon(self, key):
"""Return the icon associated with a given key.
"""
if key in self._store:
return self._store[key]["icon"]
elif self._default is not None:
return self._store[self._default]["icon"]
else:
return self._defaultIcon
return replaceMap
def reorder(self, order):
"""Reorder the items according to list.
"""
if len(order) != len(self._store):
logger.error("Length mismatch between new and old order")
return False
if order == list(self._store.keys()):
return False
store = {}
for key in order:
if key in self._store:
store[key] = self._store[key]
else:
logger.error("Unknown key '%s' in order", key)
return False
self._store = store
return True
def resetCounts(self):
"""Clear the counts of references to the status entries.
"""
self._theCounts = [0]*self._theLength
for key in self._store:
self._store[key]["count"] = 0
return
def countEntry(self, theLabel):
"""Increment the counter for a given label. This should be used
together with resetCounts in a loop over project items.
def increment(self, key):
"""Increment the counter for a given entry.
"""
theIndex = self._getIndex(theLabel)
if theIndex is not None:
self._theCounts[theIndex] += 1
if key in self._store:
self._store[key]["count"] += 1
return
def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the
main project file.
"""
for n in range(self._theLength):
for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={
"red": str(self._theColours[n][0]),
"green": str(self._theColours[n][1]),
"blue": str(self._theColours[n][2]),
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
})
xSub.text = self._theLabels[n]
xSub.text = data["name"]
return True
def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values.
"""
theLabels = []
theColours = []
self._store = {}
self._reverse = {}
self._default = None
for xChild in xParent:
theLabels.append(xChild.text)
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
theColours.append((cR, cG, cB))
if len(theLabels) > 0:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
for n in range(len(theLabels)):
self.addEntry(theLabels[n], theColours[n])
key = xChild.attrib.get("key", None)
name = xChild.text.strip()
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
self.write(key, name, (red, green, blue), count)
return True
@@ -165,39 +241,52 @@ class NWStatus():
# Internal Functions
##
def _getIndex(self, theLabel):
"""Look up a status entry in the object lists, and return it if
it exists.
def _newKey(self):
"""Generate a new key for a status flag. This method is
recursive, but should only fail if there is an issue with the
random number generator or the user has added a lot of status
flags. The Python recursion limit is given the job to handle
the extreme case and will cause an app crash.
"""
if theLabel is None:
return None
return self._theMap.get(theLabel.strip(), None)
key = f"{self._prefix}{random.getrandbits(24):06x}"
if key in self._store:
key = self._newKey()
return key
def _isKey(self, value):
"""Check if a value is a key or not.
"""
if not isinstance(value, str):
return False
if len(value) != 7:
return False
if value[0] != self._prefix:
return False
for c in value[1:]:
if c not in "0123456789abcdef":
return False
return True
##
# Iterator Bits
##
def __getitem__(self, n):
"""Return an entry by its index.
"""
if n >= 0 and n < self._theLength:
return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n]
return None, None, None, QIcon()
def __len__(self):
return len(self._store)
def __getitem__(self, key):
return self._store[key]
def __iter__(self):
"""Initialise the iterator.
"""
self._theIndex = 0
return self
return iter(self._store)
def __next__(self):
"""Return the next entry for the iterator.
"""
if self._theIndex < self._theLength:
theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex)
self._theIndex += 1
return theLabel, theColour, theCount, theIcon
else:
raise StopIteration
def keys(self):
return self._store.keys()
def items(self):
return self._store.items()
def values(self):
return self._store.values()
# END Class NWStatus