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
@@ -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
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8984" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 3.7531209,6.4345258 c -1.004161,1.002875 -1.004161,2.63319 0,3.6360652 l 8.2467181,8.248004 8.246719,-8.248004 C 20.749281,9.5691528 21,8.9108558 21,8.2525578 c 0,-0.658297 -0.250719,-1.316595 -0.753442,-1.818032 -1.004161,-1.004161 -2.631904,-1.004161 -3.636065,0 L 11.999839,11.043894 7.3891859,6.4345258 c -1.004161,-1.004161 -2.631905,-1.004161 -3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8984" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 20.246879,17.565474 c 1.004161,-1.002875 1.004161,-2.63319 0,-3.636065 L 12.000161,5.6814052 3.7534421,13.929409 c -0.502723,0.501438 -0.753442,1.159735 -0.753442,1.818033 0,0.658297 0.250719,1.316595 0.753442,1.818032 1.004161,1.004161 2.631904,1.004161 3.636065,0 l 4.6106539,-4.609368 4.610653,4.609368 c 1.004161,1.004161 2.631905,1.004161 3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -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
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8984" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 3.7531209,6.4345258 c -1.004161,1.002875 -1.004161,2.63319 0,3.6360652 l 8.2467181,8.248004 8.246719,-8.248004 C 20.749281,9.5691528 21,8.9108558 21,8.2525578 c 0,-0.658297 -0.250719,-1.316595 -0.753442,-1.818032 -1.004161,-1.004161 -2.631904,-1.004161 -3.636065,0 L 11.999839,11.043894 7.3891859,6.4345258 c -1.004161,-1.004161 -2.631905,-1.004161 -3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8984" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 20.246879,17.565474 c 1.004161,-1.002875 1.004161,-2.63319 0,-3.636065 L 12.000161,5.6814052 3.7534421,13.929409 c -0.502723,0.501438 -0.753442,1.159735 -0.753442,1.818033 0,0.658297 0.250719,1.316595 0.753442,1.818032 1.004161,1.004161 2.631904,1.004161 3.636065,0 l 4.6106539,-4.609368 4.610653,4.609368 c 1.004161,1.004161 2.631905,1.004161 3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+13
View File
@@ -184,6 +184,12 @@ def checkIntRange(value, first, last, default):
return default
def minmax(value, minVal, maxVal):
"""Make sure an integer is between min and max value (inclusive).
"""
return min(maxVal, max(minVal, value))
def checkIntTuple(value, valid, default):
"""Check that an int is an element of a tuple. If it isn't, return
the default value.
@@ -245,6 +251,13 @@ def formatTime(tS):
# String Functions
# =============================================================================================== #
def simplified(string):
"""Take a string an strip leading and trailing whitespaces, and
replace all occurences of (multiple) whitespaces with a 0x20 space.
"""
return " ".join(str(string).strip().split())
def splitVersionNumber(value):
"""Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
+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
+17 -13
View File
@@ -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
##
+101 -65
View File
@@ -35,6 +35,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwAlert
from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
@@ -81,6 +82,9 @@ class GuiProjectSettings(PagedDialog):
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
# Flags
self.spellChanged = False
logger.debug("GuiProjectSettings initialisation complete")
return
@@ -103,16 +107,18 @@ class GuiProjectSettings(PagedDialog):
self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors)
self.theProject.setSpellLang(spellLang)
self.theProject.setProjBackup(doBackup)
# Remember this as updating spell dictionary can be expensive
self.spellChanged = self.theProject.setSpellLang(spellLang)
if self.tabStatus.colChanged:
statusCol = self.tabStatus.getNewList()
self.theProject.setStatusColours(statusCol)
newList, delList = self.tabStatus.getNewList()
self.theProject.setStatusColours(newList, delList)
if self.tabImport.colChanged:
importCol = self.tabImport.getNewList()
self.theProject.setImportColours(importCol)
newList, delList = self.tabImport.getNewList()
self.theProject.setImportColours(newList, delList)
if self.tabStatus.colChanged or self.tabImport.colChanged:
self.theParent.rebuildTrees()
@@ -245,6 +251,10 @@ class GuiProjectEditStatus(QWidget):
COL_LABEL = 0
COL_USAGE = 1
KEY_ROLE = Qt.UserRole
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
@@ -267,10 +277,9 @@ class GuiProjectEditStatus(QWidget):
self.optState.getInt("GuiProjectSettings", colSetting, 130)
)
self.colData = []
self.colCounts = []
self.colDeleted = []
self.colChanged = False
self.selColour = None
self.selColour = QColor(100, 100, 100)
self.iPx = self.theTheme.baseIconSize
@@ -285,8 +294,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0)
for iName, iCol, nUse, _ in self.theStatus:
self._addItem(iName, iCol, iName, nUse)
for key, entry in self.theStatus.items():
self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls
# =============
@@ -297,6 +306,12 @@ class GuiProjectEditStatus(QWidget):
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._delItem)
self.upButton = QPushButton(self.theTheme.getIcon("up"), "")
self.upButton.clicked.connect(lambda: self._moveItem(-1))
self.dnButton = QPushButton(self.theTheme.getIcon("down"), "")
self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form
# =========
@@ -306,7 +321,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.colPixmap = QPixmap(self.iPx, self.iPx)
self.colPixmap.fill(QColor(120, 120, 120))
self.colPixmap.fill(QColor(100, 100, 100))
self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"))
self.colButton.setIconSize(self.colPixmap.rect().size())
self.colButton.clicked.connect(self._selectColour)
@@ -320,6 +335,8 @@ class GuiProjectEditStatus(QWidget):
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addWidget(self.upButton)
self.listControls.addWidget(self.dnButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
@@ -349,12 +366,15 @@ class GuiProjectEditStatus(QWidget):
if self.colChanged:
newList = []
for n in range(self.listBox.topLevelItemCount()):
nItem = self.listBox.topLevelItem(n)
nIdx = nItem.data(self.COL_LABEL, Qt.UserRole)
newList.append(self.colData[nIdx])
return newList
item = self.listBox.topLevelItem(n)
newList.append({
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
"name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
return newList, self.colDeleted
return None
return [], []
##
# User Actions
@@ -369,16 +389,16 @@ class GuiProjectEditStatus(QWidget):
)
if newCol.isValid():
self.selColour = newCol
colPixmap = QPixmap(self.iPx, self.iPx)
colPixmap.fill(newCol)
self.colButton.setIcon(QIcon(colPixmap))
self.colButton.setIconSize(colPixmap.rect().size())
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(newCol)
self.colButton.setIcon(QIcon(pixmap))
self.colButton.setIconSize(pixmap.rect().size())
return
def _newItem(self):
"""Create a new status item.
"""
newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0)
newItem = self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70)))
newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70)))
self.colChanged = True
@@ -390,14 +410,14 @@ class GuiProjectEditStatus(QWidget):
selItem = self._getSelectedItem()
if selItem is not None:
iRow = self.listBox.indexOfTopLevelItem(selItem)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
if self.colCounts[selIdx] == 0:
self.listBox.takeTopLevelItem(iRow)
self.colChanged = True
else:
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.theParent.makeAlert(self.tr(
"Cannot delete a status item that is in use."
), nwAlert.ERROR)
else:
self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
self.colChanged = True
return
def _saveItem(self):
@@ -405,53 +425,75 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
self.colData[selIdx] = (
self.editName.text().strip(),
self.selColour.red(),
self.selColour.green(),
self.selColour.blue(),
self.colData[selIdx][4]
)
selItem.setText(self.COL_LABEL, self.colData[selIdx][0])
selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx]))
selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
selItem.setData(self.COL_LABEL, self.COL_ROLE, (
self.selColour.red(), self.selColour.green(), self.selColour.blue()
))
self.editName.setEnabled(False)
self.colChanged = True
return
def _addItem(self, iName, iCol, oName, nUse):
def _addItem(self, key, name, cols, count):
"""Add a status item to the list.
"""
newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(QColor(*iCol))
newItem = QTreeWidgetItem()
newItem.setText(self.COL_LABEL, iName)
newItem.setText(self.COL_USAGE, self._usageString(nUse))
newItem.setIcon(self.COL_LABEL, QIcon(newIcon))
newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData))
self.listBox.addTopLevelItem(newItem)
self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
self.colCounts.append(nUse)
return newItem
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
item = QTreeWidgetItem()
item.setText(self.COL_LABEL, name)
item.setIcon(self.COL_LABEL, QIcon(pixmap))
item.setData(self.COL_LABEL, self.KEY_ROLE, key)
item.setData(self.COL_LABEL, self.COL_ROLE, cols)
item.setData(self.COL_LABEL, self.NUM_ROLE, count)
item.setText(self.COL_USAGE, self._usageString(count))
self.listBox.addTopLevelItem(item)
return item
def _moveItem(self, step):
"""Move and item up or down step.
"""
selItem = self._getSelectedItem()
if selItem is None:
return
tIndex = self.listBox.indexOfTopLevelItem(selItem)
nChild = self.listBox.topLevelItemCount()
nIndex = tIndex + step
if nIndex < 0 or nIndex >= nChild:
return
cItem = self.listBox.takeTopLevelItem(tIndex)
self.listBox.insertTopLevelItem(nIndex, cItem)
self.listBox.clearSelection()
cItem.setSelected(True)
self.colChanged = True
return
def _selectedItem(self):
"""Extract the info of a selected item and populate the settings
boxes and button.
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
selVal = self.colData[selIdx]
self.selColour = QColor(selVal[1], selVal[2], selVal[3])
newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(self.selColour)
self.editName.setText(selVal[0])
self.colButton.setIcon(QIcon(newIcon))
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
if selItem is None:
return
cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
name = selItem.text(self.COL_LABEL)
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
self.selColour = QColor(*cols)
self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
return
@@ -467,12 +509,6 @@ class GuiProjectEditStatus(QWidget):
return selItem[0]
return None
def _rowsMoved(self):
"""A row has been moved, so set the changed flag.
"""
self.colChanged = True
return
def _usageString(self, nUse):
"""Generate usage string.
"""
+5
View File
@@ -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.
"""
+3 -1
View File
@@ -291,8 +291,10 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("<b>%s</b>" % 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)
+1 -1
View File
@@ -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",
+4 -2
View File
@@ -545,7 +545,7 @@ class GuiMain(QMainWindow):
if self.theIndex.indexBroken:
self.makeAlert(self.tr(
"The project index is outdated or broken. Rebuilding index."
), nwAlert.WARN)
), nwAlert.INFO)
self.rebuildIndex()
# Make sure the changed status is set to false on things opened
@@ -1002,7 +1002,9 @@ class GuiMain(QMainWindow):
if dlgProj.result() == QDialog.Accepted:
logger.debug("Applying new project settings")
self.docEditor.setDictionaries()
if dlgProj.spellChanged:
self.docEditor.setDictionaries()
self.treeMeta.refreshDetails()
self._updateWindowTitle(self.theProject.projName)
return True