Fix and optimise code (#904)

* Fix bug in early error reporting in main init
* Update docstrings and optimise code in GuiMain
* Update docstrings and optimise code in Config
* Update docstrings and optimise code in common module
* Update docstrings and optimise code in main project classes
* Update the OptionState class
* Update the spell checker class
* Update the file converter classes and extend tests
* Update the about, merge, split and item editor classes and extend tests
* Update item editor test
* Update about dialog tests
* Some minor test cleanup
* Fix typo and add clarification in contributing guide
This commit is contained in:
Veronica Berglyd Olsen
2021-10-14 20:35:42 +01:00
committed by GitHub
parent 2a06db0a2b
commit 1c45331b0a
31 changed files with 1042 additions and 666 deletions
+2 -2
View File
@@ -76,13 +76,13 @@ style guide, but with a few exceptions. Some key points are listed below.
**Variable and Function Names**
* PEP8 allows for camelCase for consistency with existing code. The Qt library uses camelCase, so
the Python the source code does too.
the Python source code does too.
* The exception to the above is for constants. They should always be in upper snake case, like PEP8
states.
**Spaces, Indentation and Alignment**
* Only indentation by 4 spaces is allowed.
* Only indentation by multiples of 4 spaces is allowed.
* No trailing spaces should occur on any line in the source code, including empty lines.
* Ideally, a function should end on the same indention level as it started. Exceptions are allowed
if it makes the code easier to follow.
+10 -10
View File
@@ -117,7 +117,7 @@ def main(sysArgs=None):
# Valid Input Options
shortOpt = "hv"
longOpt = [
longOpt = [
"help",
"version",
"info",
@@ -215,31 +215,31 @@ def main(sysArgs=None):
errorCode = 0
if sys.hexversion < 0x030600f0:
errorData.append(
"At least Python 3.6.0 is required, found %s." % CONFIG.verPyString
"At least Python 3.6.0 is required, found %s" % CONFIG.verPyString
)
errorCode |= 4
if CONFIG.verQtValue < 50300:
errorData.append(
"At least Qt5 version 5.3 is required, found %s." % CONFIG.verQtString
"At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
)
errorCode |= 8
if CONFIG.verPyQtValue < 50300:
errorData.append(
"At least PyQt5 version 5.3 is required, found %s." % CONFIG.verPyQtString
"At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
)
errorCode |= 16
try:
import lxml # noqa: F401
except ImportError:
errorData.append("Python module 'lxml' is missing.")
errorData.append("Python module 'lxml' is missing")
errorCode |= 32
if errorData:
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
errDlg = QErrorMessage()
errDlg.resize(500, 300)
errDlg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
@@ -247,8 +247,8 @@ def main(sysArgs=None):
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
for errMsg in errorData:
logger.critical(errMsg)
for errLine in errorData:
logger.critical(errLine)
errApp.exec_()
sys.exit(errorCode)
+24 -13
View File
@@ -46,7 +46,7 @@ logger = logging.getLogger(__name__)
# =============================================================================================== #
def checkString(value, default, allowNone=False):
"""Check if a variable is a string or a none.
"""Check if a variable is a string or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -56,7 +56,7 @@ def checkString(value, default, allowNone=False):
def checkInt(value, default, allowNone=False):
"""Check if a variable is an integer or a none.
"""Check if a variable is an integer or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -66,8 +66,19 @@ def checkInt(value, default, allowNone=False):
return default
def checkFloat(value, default, allowNone=False):
"""Check if a variable is a float or a None.
"""
if allowNone and (value is None or value == "None"):
return None
try:
return float(value)
except Exception:
return default
def checkBool(value, default, allowNone=False):
"""Check if a variable is a boolean or a none.
"""Check if a variable is a boolean or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -211,7 +222,7 @@ def formatTime(tS):
def parseTimeStamp(theStamp, default, allowNone=False):
"""Parses a text representation of a time stamp and converts it into
"""Parses a text representation of a timestamp and converts it into
a float. Note that negative timestamps cause an OSError on Windows.
See https://bugs.python.org/issue29097
"""
@@ -228,7 +239,7 @@ def parseTimeStamp(theStamp, default, allowNone=False):
# =============================================================================================== #
def splitVersionNumber(value):
"""Splits a version string on the form aa.bb.cc into major, minor
"""Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
"""
if not isinstance(value, str):
@@ -337,7 +348,7 @@ def fuzzyTime(secDiff):
).format(int(round(secDiff/31557600)))
def numberToRoman(numVal, isLower=False):
def numberToRoman(numVal, toLower=False):
"""Convert an integer to a Roman number.
"""
if not isinstance(numVal, int):
@@ -358,7 +369,7 @@ def numberToRoman(numVal, isLower=False):
if numVal <= 0:
break
return romNum.lower() if isLower else romNum
return romNum.lower() if toLower else romNum
# =============================================================================================== #
@@ -434,11 +445,11 @@ def readTextFile(filePath):
return fileText
def makeFileNameSafe(theText):
"""Returns a filename safe version of the text.
def makeFileNameSafe(value):
"""Returns a filename safe string of the value.
"""
cleanName = ""
for c in theText.strip():
for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
@@ -456,7 +467,7 @@ def sha256sum(filePath):
for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n])
except Exception:
logger.error("Could not read sha256sum of: %s", filePath)
logger.error("Could not create sha256sum of: %s", filePath)
logException()
return None
@@ -467,11 +478,11 @@ def sha256sum(filePath):
# Other Functions
# =============================================================================================== #
def getGuiItem(theName):
def getGuiItem(objName):
"""Returns a QtWidget based on its objectName.
"""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
if qWidget.objectName() == objName:
return qWidget
return None
+65 -30
View File
@@ -302,7 +302,7 @@ class Config:
logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath)
# If config folder does not exist, create it.
# If the config folder does not exist, create it.
# This assumes that the os config folder itself exists.
if not os.path.isdir(self.confPath):
try:
@@ -324,7 +324,7 @@ class Config:
# If it does not exist, save a copy of the default values
self.saveConfig()
# If data folder does not exist, make it.
# If the data folder does not exist, create it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not os.path.isdir(self.dataPath):
@@ -534,7 +534,7 @@ class Config:
logger.info("Using straight single quotes, so disabling auto-replace")
self.doReplaceSQuote = False
if self.fmtDoubleQuotes == ["\"", "\""] and self.doReplaceDQuote:
if self.fmtDoubleQuotes == ['"', '"'] and self.doReplaceDQuote:
logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False
@@ -671,36 +671,28 @@ class Config:
if self.dataPath is None:
return False
cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {}
if os.path.isfile(cacheFile):
try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile)
cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
if not os.path.isfile(cacheFile):
return True
for projPath in theData.keys():
theEntry = theData[projPath]
theTitle = ""
lastTime = 0
wordCount = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
lastTime = int(theEntry["time"])
if "words" in theEntry.keys():
wordCount = int(theEntry["words"])
self.recentProj[projPath] = {
"title": theTitle,
"time": lastTime,
"words": wordCount,
}
try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile)
except Exception as e:
self.hasError = True
self.errData.append("Could not load recent project cache")
self.errData.append(str(e))
return False
for projPath, theEntry in theData.items():
self.recentProj[projPath] = {
"title": theEntry.get("title", ""),
"time": theEntry.get("time", 0),
"words": theEntry.get("words", 0),
}
except Exception as e:
self.hasError = True
self.errData.append("Could not load recent project cache")
self.errData.append(str(e))
return False
return True
@@ -755,6 +747,8 @@ class Config:
##
def setConfPath(self, newPath):
"""Set the path and filename to the config file.
"""
if newPath is None:
return True
if not os.path.isfile(newPath):
@@ -765,6 +759,8 @@ class Config:
return True
def setDataPath(self, newPath):
"""Set the data path.
"""
if newPath is None:
return True
if not os.path.isdir(newPath):
@@ -774,6 +770,8 @@ class Config:
return True
def setLastPath(self, lastPath):
"""Set the last used path (by the user).
"""
if lastPath is None or lastPath == "":
self.lastPath = ""
else:
@@ -781,6 +779,11 @@ class Config:
return True
def setWinSize(self, newWidth, newHeight):
"""Set the size of the main window, but only if the change is
larger than 5 pixels. The OS window manager will sometimes
adjust it a bit, and we don't want the main window to shrink or
grow each time the app is opened.
"""
newWidth = int(newWidth/self.guiScale)
newHeight = int(newHeight/self.guiScale)
if abs(self.winGeometry[0] - newWidth) > 5:
@@ -792,57 +795,79 @@ class Config:
return True
def setPreferencesSize(self, newWidth, newHeight):
"""Sat the size of the Preferences dialog window.
"""
self.prefGeometry[0] = int(newWidth/self.guiScale)
self.prefGeometry[1] = int(newHeight/self.guiScale)
self.confChanged = True
return True
def setTreeColWidths(self, colWidths):
"""Set the column widths of the main project tree.
"""
self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setNovelColWidths(self, colWidths):
"""Set the column widths of the novel tree.
"""
self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setProjColWidths(self, colWidths):
"""Set the column widths of the Load Project dialog.
"""
self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setMainPanePos(self, panePos):
"""Set the position of the main GUI splitter.
"""
self.mainPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setDocPanePos(self, panePos):
"""Set the position of the main editor/viewer splitter.
"""
self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setViewPanePos(self, panePos):
"""Set the position of the viewer meta data splitter.
"""
self.viewPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setOutlinePanePos(self, panePos):
"""Set the position of the outline details splitter.
"""
self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setShowRefPanel(self, checkState):
"""Set the visibility state of the reference panel.
"""
self.showRefPanel = checkState
self.confChanged = True
return self.showRefPanel
def setViewComments(self, viewState):
"""Set the visibility state of comments in the viewer.
"""
self.viewComments = viewState
self.confChanged = True
return self.viewComments
def setViewSynopsis(self, viewState):
"""Set the visibility state of synopsis comments in the viewer.
"""
self.viewSynopsis = viewState
self.confChanged = True
return self.viewSynopsis
@@ -852,12 +877,18 @@ class Config:
##
def setDefaultGuiTheme(self):
"""Reset the GUI theme to default value.
"""
self.guiTheme = "default"
def setDefaultSyntaxTheme(self):
"""Reset the syntax theme to default value.
"""
self.guiSyntax = "default_light"
def setDefaultIconTheme(self):
"""Reset the icon theme to default value.
"""
self.guiIcons = "typicons_light"
##
@@ -904,6 +935,9 @@ class Config:
return self.pxInt(self.focusWidth)
def getErrData(self):
"""Compile and return error messages from the initialisation of
the Config class, and clear the error buffer.
"""
errMessage = "<br>".join(self.errData)
self.hasError = False
self.errData = []
@@ -914,7 +948,8 @@ class Config:
##
def _packList(self, inData):
"""Pack a list of items into a comma-separated string.
"""Pack a list of items into a comma-separated string for saving
to the config file.
"""
return ", ".join([str(inVal) for inVal in inData])
+8 -11
View File
@@ -287,10 +287,8 @@ class NWIndex():
nLine = 0
nTitle = 0
theLines = theText.splitlines()
for aLine in theLines:
nLine += 1
nChar = len(aLine.strip())
if nChar == 0:
for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0:
continue
if aLine.startswith("#"):
@@ -363,7 +361,7 @@ class NWIndex():
else:
return False
sTitle = "T%06d" % nLine
sTitle = f"T{nLine:06d}"
self._fileIndex[tHandle][sTitle] = {
"level": hDepth,
"title": hText,
@@ -391,14 +389,13 @@ class NWIndex():
"pCount": 0,
"synopsis": "",
}
return
def _indexWordCounts(self, tHandle, theText, nTitle):
"""Count text stats and save the counts to the index.
"""
cC, wC, pC = countWords(theText)
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["cCount"] = cC
@@ -409,7 +406,7 @@ class NWIndex():
def _indexSynopsis(self, tHandle, theText, nTitle):
"""Save the synopsis to the index.
"""
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["synopsis"] = theText
@@ -428,7 +425,7 @@ class NWIndex():
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return
sTitle = "T%06d" % nTitle
sTitle = f"T{nTitle:06d}"
if theBits[0] == nwKeyWords.TAG_KEY:
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
@@ -525,7 +522,7 @@ class NWIndex():
"""
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._fileIndex[tHandle]):
tKey = "%s:%s" % (tHandle, sTitle)
tKey = f"{tHandle}:{sTitle}"
yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle]
def getNovelWordCount(self, skipExcluded=True):
@@ -559,7 +556,7 @@ class NWIndex():
return theCounts
for sTitle, sData in hRecord.items():
theCounts.append(("%s:%s" % (tHandle, sTitle), sData["wCount"]))
theCounts.append((f"{tHandle}:{sTitle}", sData["wCount"]))
return theCounts
+6 -15
View File
@@ -103,11 +103,8 @@ class NWItem():
logger.error("XML item entry does not have a handle")
return False
if "parent" in xItem.attrib:
self.setParent(xItem.attrib["parent"])
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
self.setParent(xItem.attrib.get("parent", None))
self.setOrder(xItem.attrib.get("order", 0))
tmpStatus = ""
for xValue in xItem:
@@ -200,11 +197,8 @@ class NWItem():
def setHandle(self, theHandle):
"""Set the item handle, and ensure it is valid.
"""
if isinstance(theHandle, str):
if isHandle(theHandle):
self.itemHandle = theHandle
else:
self.itemHandle = None
if isHandle(theHandle):
self.itemHandle = theHandle
else:
self.itemHandle = None
return
@@ -214,11 +208,8 @@ class NWItem():
"""
if theParent is None:
self.itemParent = None
elif isinstance(theParent, str):
if isHandle(theParent):
self.itemParent = theParent
else:
self.itemParent = None
elif isHandle(theParent):
self.itemParent = theParent
else:
self.itemParent = None
return
+70 -130
View File
@@ -30,88 +30,40 @@ import logging
import novelwriter
from novelwriter.constants import nwFiles
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
logger = logging.getLogger(__name__)
VALID_MAP = {
"GuiWritingStats": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
},
"GuiDocSplit": {"spLevel"},
"GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "addNovel",
"addNotes", "ignoreFlag", "justifyText", "excludeBody", "textFont",
"textSize", "lineHeight", "noStyling", "incSynopsis", "incComments",
"incKeywords", "incBodyText", "replaceTabs", "replaceUCode"
},
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
"GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
},
"GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
},
"GuiWordList": {"winWidth", "winHeight"}
}
class OptionState():
def __init__(self, theProject):
self.theProject = theProject
self.theState = {}
self.validMap = {
"GuiWritingStats": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"sortCol",
"sortOrder",
"incNovel",
"incNotes",
"hideZeros",
"hideNegative",
"groupByDay",
"showIdleTime",
"histMax",
},
"GuiDocSplit": {
"spLevel",
},
"GuiBuildNovel": {
"winWidth",
"winHeight",
"boxWidth",
"docWidth",
"addNovel",
"addNotes",
"ignoreFlag",
"justifyText",
"excludeBody",
"textFont",
"textSize",
"lineHeight",
"noStyling",
"incSynopsis",
"incComments",
"incKeywords",
"incBodyText",
"replaceTabs",
"replaceUCode",
},
"GuiOutline": {
"headerOrder",
"columnWidth",
"columnHidden",
},
"GuiProjectSettings": {
"winWidth",
"winHeight",
"replaceColW",
"statusColW",
"importColW",
},
"GuiProjectDetails": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"widthCol4",
"wordsPerPage",
"countFrom",
"clearDouble",
},
"GuiWordList": {
"winWidth",
"winHeight",
}
}
self._theState = {}
return
##
@@ -139,11 +91,11 @@ class OptionState():
# Filter out unused variables
for aGroup in theState:
if aGroup in self.validMap:
self.theState[aGroup] = {}
if aGroup in VALID_MAP:
self._theState[aGroup] = {}
for anOpt in theState[aGroup]:
if anOpt in self.validMap[aGroup]:
self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
if anOpt in VALID_MAP[aGroup]:
self._theState[aGroup][anOpt] = theState[aGroup][anOpt]
return True
@@ -158,7 +110,7 @@ class OptionState():
try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self.theState, outFile, indent=2)
json.dump(self._theState, outFile, indent=2)
except Exception:
logger.error("Failed to save GUI options file")
novelwriter.logException()
@@ -170,21 +122,21 @@ class OptionState():
# Setters
##
def setValue(self, setGroup, setName, setValue):
def setValue(self, group, name, value):
"""Saves a value, with a given group and name.
"""
if setGroup not in self.validMap:
logger.error("Unknown option group '%s'", setGroup)
if group not in VALID_MAP:
logger.error("Unknown option group '%s'", group)
return False
if setName not in self.validMap[setGroup]:
logger.error("Unknown option name '%s'", setName)
if name not in VALID_MAP[group]:
logger.error("Unknown option name '%s'", name)
return False
if setGroup not in self.theState:
self.theState[setGroup] = {}
if group not in self._theState:
self._theState[group] = {}
self.theState[setGroup][setName] = setValue
self._theState[group][name] = value
return True
@@ -192,79 +144,67 @@ class OptionState():
# Getters
##
def getValue(self, getGroup, getName, defaultValue):
def getValue(self, group, name, default):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return self.theState[getGroup][getName]
return defaultValue
if group in self._theState:
return self._theState[group].get(name, default)
return default
def getString(self, getGroup, getName, defaultValue):
def getString(self, group, name, default):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return str(self.theState[getGroup][getName])
return defaultValue
if group in self._theState:
return checkString(self._theState[group].get(name, default), default)
return default
def getInt(self, getGroup, getName, defaultValue):
def getInt(self, group, name, default):
"""Return the value as an int, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return int(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
if group in self._theState:
return checkInt(self._theState[group].get(name, default), default)
return default
def getFloat(self, getGroup, getName, defaultValue):
def getFloat(self, group, name, default):
"""Return the value as a float, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return float(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
if group in self._theState:
return checkFloat(self._theState[group].get(name, default), default)
return default
def getBool(self, getGroup, getName, defaultValue):
def getBool(self, group, name, default):
"""Return the value as a bool, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
return bool(self.theState[getGroup][getName])
return defaultValue
if group in self._theState:
if name in self._theState[group]:
return checkBool(self._theState[group].get(name, default), default)
return default
##
# Validators
##
def validIntRange(self, theValue, intA, intB, intDefault):
def validIntRange(self, value, first, last, default):
"""Check that an int is in a given range. If it isn't, return
the default value.
"""
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
if isinstance(value, int):
if value >= first and value <= last:
return value
return default
def validIntTuple(self, theValue, theTuple, intDefault):
def validIntTuple(self, value, valid, default):
"""Check that an int is an element of a tuple. If it isn't,
return the default value.
"""
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
if isinstance(value, int):
if value in valid:
return value
return default
# END Class OptionState
+9 -8
View File
@@ -125,6 +125,7 @@ class NWProject():
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
newItem = NWItem(self)
newItem.setName(rootName)
newItem.setType(nwItemType.ROOT)
@@ -356,7 +357,7 @@ class NWProject():
return True
def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if doesn't exist, assume
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
@@ -469,8 +470,8 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
# 1.3 : Reduces the number of layouts to onlye two. One for
# novel documents and one for notes. Introduced in version
# 1.5.
# novel documents and one for project notes. Introduced in
# version 1.5.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
self.theParent.makeAlert(self.tr(
@@ -973,7 +974,7 @@ class NWProject():
return False
self.bookAuthors = []
for bookAuthor in bookAuthors.split("\n"):
for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip()
if bookAuthor == "":
continue
@@ -1074,7 +1075,7 @@ class NWProject():
replaceMap = self.statusItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass == nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys():
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1086,7 +1087,7 @@ class NWProject():
replaceMap = self.importItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass != nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys():
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1104,7 +1105,7 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
return True
def setProjectChanged(self, bValue):
@@ -1346,7 +1347,7 @@ class NWProject():
return
def _packProjectKeyValue(self, xParent, theName, theDict):
"""Pack the entries in the auto-replace dictionary.
"""Pack the entries of a dictionary into an xml element.
"""
xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items():
+40 -28
View File
@@ -35,16 +35,24 @@ class NWSpellEnchant():
def __init__(self):
self.mainConf = novelwriter.CONFIG
self.theDict = None
self.projDict = set()
self.projectDict = None
self.spellLanguage = None
self.theBroker = None
self._theDict = None
self._projDict = set()
self._projectDict = None
self._spellLanguage = None
self._theBroker = None
logger.debug("Enchant spell checking activated")
return
##
# Getters and Setters
##
def spellLanguage(self):
return self._spellLanguage
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't
@@ -52,49 +60,53 @@ class NWSpellEnchant():
"""
try:
import enchant
if self.theBroker is not None:
if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker")
del self.theBroker
del self._theBroker
self.theBroker = enchant.Broker()
self.theDict = self.theBroker.request_dict(theLang)
self.spellLanguage = theLang
self._theBroker = enchant.Broker()
self._theDict = self._theBroker.request_dict(theLang)
self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
self.theDict = FakeEnchant()
self.spellLanguage = None
self._theDict = FakeEnchant()
self._spellLanguage = None
self._readProjectDictionary(projectDict)
for pWord in self.projDict:
self.theDict.add_to_session(pWord)
for pWord in self._projDict:
self._theDict.add_to_session(pWord)
return
##
# Methods
##
def checkWord(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.check(theWord)
return self._theDict.check(theWord)
def suggestWords(self, theWord):
"""Wrapper function for pyenchant.
"""
return self.theDict.suggest(theWord)
return self._theDict.suggest(theWord)
def addWord(self, newWord):
"""Add a word to the project dictionary.
"""
self.theDict.add_to_session(newWord)
self._theDict.add_to_session(newWord)
if self.projectDict is not None and newWord not in self.projDict:
if self._projectDict is not None and newWord not in self._projDict:
newWord = newWord.strip()
try:
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
with open(self._projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.add(newWord)
self._projDict.add(newWord)
except Exception:
logger.error("Failed to add word to project word list %s", str(self.projectDict))
logger.error("Failed to add word to project word list %s", str(self._projectDict))
novelwriter.logException()
return False
return True
@@ -119,8 +131,8 @@ class NWSpellEnchant():
dictionary.
"""
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
spTag = self._theDict.tag
spName = self._theDict.provider.name
except Exception:
logger.error("Failed to extract information about the dictionary")
novelwriter.logException()
@@ -137,8 +149,8 @@ class NWSpellEnchant():
"""Read the content of the project dictionary, and add it to the
lookup lists.
"""
self.projDict = set()
self.projectDict = projectDict
self._projDict = set()
self._projectDict = projectDict
if projectDict is None:
return False
@@ -151,9 +163,9 @@ class NWSpellEnchant():
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self.projDict))
if len(theLine) > 0 and theLine not in self._projDict:
self._projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self._projDict))
except Exception:
logger.error("Failed to load project word list")
+59 -63
View File
@@ -109,7 +109,7 @@ class ToHtml(Tokenizer):
to theResult.
"""
if self.genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2
htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>",
self.FMT_I_B: "<i>",
@@ -118,7 +118,7 @@ class ToHtml(Tokenizer):
self.FMT_D_E: "</span>",
}
else:
htmlTags = { # HTML5
htmlTags = { # HTML5 (for export)
self.FMT_B_B: "<strong>",
self.FMT_B_E: "</strong>",
self.FMT_I_B: "<em>",
@@ -176,17 +176,18 @@ class ToHtml(Tokenizer):
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
aStyle.append("margin-left: %dpx;" % self.mainConf.tabWidth)
aStyle.append(f"margin-left: {self.mainConf.tabWidth:d}px;")
if tStyle & self.A_IND_R:
aStyle.append("margin-right: %dpx;" % self.mainConf.tabWidth)
aStyle.append(f"margin-right: {self.mainConf.tabWidth:d}px;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
stVals = " ".join(aStyle)
hStyle = f" style='{stVals}'"
else:
hStyle = ""
if self.linkHeaders:
aNm = "<a name='T%06d'></a>" % tLine
aNm = f"<a name='T{tLine:06d}'></a>"
else:
aNm = ""
@@ -200,36 +201,36 @@ class ToHtml(Tokenizer):
parClass = ""
if len(thisPar) > 0:
tTemp = "<br/>".join(thisPar)
tmpResult.append("<p%s%s>%s</p>\n" % (parClass, parStyle, tTemp.rstrip()))
tmpResult.append(f"<p{parClass+parStyle}>{tTemp.rstrip()}</p>\n")
thisPar = []
parStyle = None
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1 class='title'%s>%s%s</h1>\n" % (hStyle, aNm, tHead))
tmpResult.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h2, hStyle, aNm, tHead, h2))
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h3, hStyle, aNm, tHead, h3))
tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h4, hStyle, aNm, tHead, h4))
tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
elif tType == self.T_SEP:
tmpResult.append("<p class='sep'>%s</p>\n" % tText)
tmpResult.append(f"<p class='sep'>{tText}</p>\n")
elif tType == self.T_SKIP:
tmpResult.append("<p class='skip'>&nbsp;</p>\n")
@@ -249,7 +250,7 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tTemp = "<p%s>%s</p>\n" % (hStyle, self._formatKeywords(tText))
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
@@ -298,9 +299,9 @@ class ToHtml(Tokenizer):
"""Replace tabs with spaces in the html.
"""
htmlText = []
eightSpace = spaceChar*nSpaces
tabSpace = spaceChar*nSpaces
for aLine in self.fullHTML:
htmlText.append(aLine.replace("\t", eightSpace))
htmlText.append(aLine.replace("\t", tabSpace))
self.fullHTML = htmlText
return
@@ -315,75 +316,76 @@ class ToHtml(Tokenizer):
mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left"
theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % (
theStyles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
self.textFont, self.textSize
))
theStyles.append((
"p {"
"text-align: %s; line-height: %d%%; "
"margin-top: %.2fem; margin-bottom: %.2fem;"
"}"
) % (
"p {{"
"text-align: {0}; line-height: {1:d}%; "
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}}"
).format(
textAlign,
round(100 * self.lineHeight),
mScale * self.marginText[0],
mScale * self.marginText[1],
))
theStyles.append((
"h1 {"
"h1 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
))
theStyles.append((
"h2 {"
"h2 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
))
theStyles.append((
"h3 {"
"h3 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
))
theStyles.append((
"h4 {"
"h4 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
))
theStyles.append((
".title {"
".title {{"
"font-size: 2.5em; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
))
theStyles.append((
".sep, .skip {"
".sep, .skip {{"
"text-align: center; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;}"
) % (
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale, mScale
))
@@ -421,31 +423,25 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span> " % nwLabels.KEY_NAME[theBits[0]]
retText += f"<span class='tags'>{nwLabels.KEY_NAME[theBits[0]]}:</span> "
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'>%s</a>" % (
theBits[1], theBits[1]
)
retText += f"<a name='tag_{theBits[1]}'>{theBits[1]}</a>"
else:
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
refTags.append(f"<a href='#{theBits[0][1:]}={tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
else:
for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % (
tTag, tTag
))
refTags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
retText += ", ".join(refTags)
return retText
+5 -4
View File
@@ -267,13 +267,14 @@ class Tokenizer():
else:
textAlign = self.A_PBB | self.A_CENTRE
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}"
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, textAlign
))
if self.keepMarkdown:
self.theMarkdown.append("# %s\n\n" % theTitle)
self.theMarkdown.append(f"# {theTitle}\n\n")
return True
@@ -302,7 +303,7 @@ class Tokenizer():
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
)
self.theText = "# %s\n\n%s\n\n" % (self.tr("ERROR"), errVal)
self.theText = "# {0}\n\n{1}\n\n".format(self.tr("ERROR"), errVal)
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
@@ -318,7 +319,7 @@ class Tokenizer():
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
repDict[f"<{aKey}>"] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
+13 -11
View File
@@ -100,33 +100,33 @@ class ToMarkdown(Tokenizer):
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = " \n".join(thisPar)
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
tTemp = (" \n".join(thisPar)).rstrip(" ")
tmpResult.append(f"{tTemp}\n\n")
thisPar = []
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("### %s\n\n" % tHead)
tmpResult.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("#### %s\n\n" % tHead)
tmpResult.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP:
tmpResult.append("%s\n\n" % tText)
@@ -141,10 +141,12 @@ class ToMarkdown(Tokenizer):
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
locName = self._localLookup("Synopsis")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
locName = self._localLookup("Comment")
tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
@@ -189,7 +191,7 @@ class ToMarkdown(Tokenizer):
retText = ""
if theBits[0] in nwLabels.KEY_NAME:
retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]]
retText += f"**{nwLabels.KEY_NAME[theBits[0]]}:** "
if len(theBits) > 1:
retText += ", ".join(theBits[1:])
+40 -37
View File
@@ -45,18 +45,35 @@ XML_NS = {
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
MANI_NS = {
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
}
OFFICE_NS = {
"office": XML_NS["office"]
}
def _mkTag(nsName, tagName, nsMap=XML_NS):
"""Assemble namespace and tag name.
"""
theNS = nsMap.get(nsName, "")
if theNS:
return f"{{{theNS}}}{tagName}"
logger.warning("Missing xml namespace '%s'", nsName)
return tagName
# Mimetype and Version
X_MIME = "application/vnd.oasis.opendocument.text"
X_VERS = "1.2"
# Text Formatting Tags
TAG_BR = "{%s}line-break" % XML_NS["text"]
TAG_SPC = "{%s}s" % XML_NS["text"]
TAG_NSPC = "{%s}c" % XML_NS["text"]
TAG_TAB = "{%s}tab" % XML_NS["text"]
TAG_SPAN = "{%s}span" % XML_NS["text"]
TAG_STNM = "{%s}style-name" % XML_NS["text"]
TAG_BR = _mkTag("text", "line-break")
TAG_SPC = _mkTag("text", "s")
TAG_NSPC = _mkTag("text", "c")
TAG_TAB = _mkTag("text", "tab")
TAG_SPAN = _mkTag("text", "span")
TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes
X_BLD = 0x01 # Bold format
@@ -313,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -472,24 +489,22 @@ class ToOdt(Tokenizer):
def saveOpenDocText(self, savePath):
"""Save the data to an .odt file.
"""
mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
mMani = "{%s}manifest" % mMap["manifest"]
mVers = "{%s}version" % mMap["manifest"]
mPath = "{%s}full-path" % mMap["manifest"]
mType = "{%s}media-type" % mMap["manifest"]
mFile = "{%s}file-entry" % mMap["manifest"]
mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=mMap)
xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=MANI_NS)
etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
oRoot = "{%s}document-settings" % sMap["office"]
oSett = "{%s}settings" % sMap["office"]
xSett = etree.Element(oRoot, nsmap=sMap)
oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
oSett = _mkTag("office", "settings", nsMap=OFFICE_NS)
xSett = etree.Element(oRoot, nsmap=OFFICE_NS)
etree.SubElement(xSett, oSett)
with ZipFile(savePath, mode="w") as outFile:
@@ -520,16 +535,16 @@ class ToOdt(Tokenizer):
"""Apply formatting to synopsis lines.
"""
sSynop = self._localLookup("Synopsis")
rTxt = "**%s:** %s" % (sSynop, tText)
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
rTxt = "**{0}:** {1}".format(sSynop, tText)
rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
sComm = self._localLookup("Comment")
rTxt = "**%s:** %s" % (sComm, tText)
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
rTxt = "**{0}:** {1}".format(sComm, tText)
rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
@@ -543,8 +558,8 @@ class ToOdt(Tokenizer):
rFmt = ""
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText
rFmt += "_B%s b_ " % (" "*len(tText))
rTxt += "**{0}:** ".format(tText)
rFmt += "_B{0} b_ ".format(" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += theBits[1]
@@ -1468,16 +1483,4 @@ class XMLParagraph():
return
# =============================================================================================== #
# Local Functions
# =============================================================================================== #
def _mkTag(nsName, tagName):
"""Assemble namespace and tag name.
"""
theNS = XML_NS.get(nsName, "")
if theNS:
return "{%s}%s" % (theNS, tagName)
logger.warning("Missing xml namespace '%s'", nsName)
return tagName
# END Class XMLParagraph
+5 -5
View File
@@ -158,7 +158,7 @@ class NWTree():
continue
tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
tocLine = "%-25s %-9s %-8s %s" % (
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
os.path.join("content", tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
@@ -175,7 +175,7 @@ class NWTree():
outFile.write("Table of Contents\n")
outFile.write("=================\n")
outFile.write("\n")
outFile.write("%-25s %-9s %-8s %s\n" % (
outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
"File Name", "Class", "Layout", "Document Label"
))
outFile.write("-"*max(tocLen, 62) + "\n")
@@ -285,12 +285,12 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for i in range(nwConst.MAX_DEPTH + 1):
for _ in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tTree
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
else:
@@ -341,7 +341,7 @@ class NWTree():
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
logger.error("Item %s is not a file", tHandle)
logger.error("Item '%s' is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
return False
+6 -6
View File
@@ -65,7 +65,7 @@ class GuiAbout(QDialog):
self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("<b>novelWriter</b>")
self.lblVers = QLabel("v%s" % novelwriter.__version__)
self.lblVers = QLabel(f"v{novelwriter.__version__}")
self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x"))
self.leftBox = QVBoxLayout()
@@ -178,7 +178,7 @@ class GuiAbout(QDialog):
),
)
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Translations"),
self._wrapTable([
("English", "Veronica Berglyd Olsen"),
@@ -193,7 +193,7 @@ class GuiAbout(QDialog):
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName and theTheme.themeAuthor != "N/A":
licURL = f"<a href='{theTheme.themeLicenseUrl}'>{theTheme.themeLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Theme: {0}").format(theTheme.themeName),
self._wrapTable([
(self.tr("Author"), theTheme.themeAuthor),
@@ -204,7 +204,7 @@ class GuiAbout(QDialog):
if theIcons.themeName:
licURL = f"<a href='{theIcons.themeLicenseUrl}'>{theIcons.themeLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Icons: {0}").format(theIcons.themeName),
self._wrapTable([
(self.tr("Author"), theIcons.themeAuthor),
@@ -215,7 +215,7 @@ class GuiAbout(QDialog):
if theTheme.syntaxName:
licURL = f"<a href='{theTheme.syntaxLicenseUrl}'>{theTheme.syntaxLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Syntax: {0}").format(theTheme.syntaxName),
self._wrapTable([
(self.tr("Author"), theTheme.syntaxAuthor),
@@ -258,7 +258,7 @@ class GuiAbout(QDialog):
theTable.append(
f"<tr><td><b>{aLabel}:</b></td><td>{aValue}</td></tr>"
)
return "<table>%s</table>" % "".join(theTable)
return "<table>{0}</table>".format("".join(theTable))
def _setStyleSheet(self):
"""Set stylesheet for all browser tabs
+1 -1
View File
@@ -55,7 +55,7 @@ class GuiDocMerge(QDialog):
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>%s</b>" % self.tr("Documents to Merge"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText
)
+8 -10
View File
@@ -59,7 +59,7 @@ class GuiDocSplit(QDialog):
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("<b>%s</b>" % self.tr("Document Headers"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
self.helpLabel = QHelpLabel(
self.tr("Select the maximum level to split into files."),
self.theParent.theTheme.helpText
@@ -174,7 +174,7 @@ class GuiDocSplit(QDialog):
msgYes = self.theParent.askQuestion(
self.tr("Split Document"),
"%s<br><br>%s" % (
"{0}<br><br>{1}".format(
self.tr(
"The document will be split into {0} file(s) in a new folder. "
"The original document will remain intact.").format(nFiles),
@@ -196,10 +196,8 @@ class GuiDocSplit(QDialog):
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
if srcItem.itemClass == nwItemClass.NOVEL:
itemLayout = nwItemLayout.DOCUMENT
else:
itemLayout = nwItemLayout.NOTE
isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
@@ -209,9 +207,8 @@ class GuiDocSplit(QDialog):
newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus)
logger.verbose(
"Creating new document %s with text from line %d to %d" % (
nHandle, iStart+1, iEnd
)
"Creating new document '%s' with text from line %d to %d",
nHandle, iStart+1, iEnd
)
theText = "\n".join(self.sourceText[iStart:iEnd])
@@ -273,7 +270,8 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug(
"Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
"Scanning document '%s' for headings level <= %d",
self.sourceItem, spLevel
)
self.sourceText = theText.splitlines()
+2 -1
View File
@@ -57,7 +57,8 @@ class GuiItemEditor(QDialog):
self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None:
self._doClose()
self.close()
return
self.setWindowTitle(self.tr("Item Settings"))
+1 -1
View File
@@ -719,7 +719,7 @@ class GuiDocEditor(QTextEdit):
), nwAlert.INFO)
theMode = False
if self.spEnchant.spellLanguage is None:
if self.spEnchant.spellLanguage() is None:
theMode = False
self._spellCheck = theMode
+18 -20
View File
@@ -784,7 +784,7 @@ class GuiMain(QMainWindow):
def passDocumentAction(self, theAction):
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
its clid widgets have focus. If neither has focus, ignore the
its child widgets have focus. If neither has focus, ignore the
action.
"""
if self.docViewer.hasFocus():
@@ -836,13 +836,13 @@ class GuiMain(QMainWindow):
if tHandle is None:
logger.warning("No item selected")
return
return False
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return
return False
if tItem.itemType not in nwLists.REG_TYPES:
return
return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
@@ -853,7 +853,7 @@ class GuiMain(QMainWindow):
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
return
return True
def rebuildTrees(self):
"""Rebuild the project tree.
@@ -938,7 +938,7 @@ class GuiMain(QMainWindow):
##
def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing
"""Open the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached. Selecting to create a
new project is forwarded to the new project wizard.
@@ -1058,7 +1058,7 @@ class GuiMain(QMainWindow):
return
def showWritingStatsDialog(self):
"""Open the session log dialog.
"""Open the session stats dialog.
"""
if not self.hasProject:
logger.error("No project open")
@@ -1092,17 +1092,17 @@ class GuiMain(QMainWindow):
if showNotes:
dlgAbout.showReleaseNotes()
return
return True
def showAboutQtDialog(self):
"""Show the about dialog for Qt.
"""
msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt")
return
return True
def showUpdatesDialog(self):
"""Show the updates dialog for novelWriter.
"""Show the check for updates dialog.
"""
dlgUpdate = getGuiItem("GuiUpdates")
if dlgUpdate is None:
@@ -1117,11 +1117,11 @@ class GuiMain(QMainWindow):
return
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message
can be either a string or an array of strings.
"""Alert both the user and the logger at the same time. The
message can be either a string or a list of strings.
"""
if isinstance(theMessage, list):
theMessage = list(filter(None, theMessage))
theMessage = list(filter(None, theMessage)) # Strip empty strings
popMsg = "<br>".join(theMessage)
logMsg = theMessage
else:
@@ -1246,13 +1246,11 @@ class GuiMain(QMainWindow):
self.theProject.setLastViewed(None)
bPos = self.splitMain.sizes()
self.splitView.setVisible(False)
vPos = [bPos[1], 0]
self.splitDocs.setSizes(vPos)
self.splitDocs.setSizes([bPos[1], 0])
return not self.splitView.isVisible()
def toggleFocusMode(self):
"""Main GUI Focus Mode hides tree, view pane and optionally also
statusbar and menu.
"""Main GUI Focus Mode hides tree, view, statusbar and menu.
"""
if self.docEditor.docHandle() is None:
logger.error("No document open, so not activating Focus Mode")
@@ -1409,7 +1407,7 @@ class GuiMain(QMainWindow):
return True
def _autoSaveProject(self):
"""Triggered by the auto-save project timer to save the project.
"""Triggered by the autosave project timer to save the project.
"""
doSave = self.hasProject
doSave &= self.theProject.projChanged
@@ -1422,7 +1420,7 @@ class GuiMain(QMainWindow):
return
def _autoSaveDocument(self):
"""Triggered by the auto-save document timer to save the
"""Triggered by the autosave document timer to save the
document.
"""
if self.hasProject and self.docEditor.docChanged():
@@ -1511,7 +1509,7 @@ class GuiMain(QMainWindow):
@pyqtSlot()
def _timeTick(self):
"""Triggered on every tick of the timer.
"""Triggered on every tick of the main timer.
"""
if not self.hasProject:
return
@@ -1,115 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 03:04:11">
<project>
<name>New Project</name>
<title></title>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>0e17daca5f3e1</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>6</lastWordCount>
<novelWordCount>6</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="31489056e0916" order="1" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
<name>Just a Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Note</status>
<exported>False</exported>
<layout>DOCUMENT</layout>
<charCount>9</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="2" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
</content>
</novelWriterXML>
+16 -2
View File
@@ -31,8 +31,8 @@ from tools import writeFile
from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkString, checkInt, checkBool, checkHandle, isHandle, isTitleTag,
isItemClass, isItemType, isItemLayout, hexToInt, formatInt,
checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, formatInt,
formatTimeStamp, formatTime, parseTimeStamp, splitVersionNumber,
transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile,
makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
@@ -68,6 +68,20 @@ def testBaseCommon_CheckInt():
# END Test testBaseCommon_CheckInt
@pytest.mark.base
def testBaseCommon_CheckFloat():
"""Test the checkFloat function.
"""
assert checkFloat(None, 3.0, True) is None
assert checkFloat("None", 3.0, True) is None
assert checkFloat(None, 3.0, False) == 3.0
assert checkFloat(1, 3.0, False) == 1.0
assert checkFloat(1.0, 3.0, False) == 1.0
assert checkFloat(True, 3.0, False) == 1.0
# END Test testBaseCommon_CheckInt
@pytest.mark.base
def testBaseCommon_CheckBool():
"""Test the checkBool function.
+3 -3
View File
@@ -73,7 +73,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
assert theOpts.loadSettings()
# Check that unwanted items have been removed
assert theOpts.theState == {
assert theOpts._theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -88,7 +88,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
# Load again to check we get the values back
assert theOpts.loadSettings()
assert theOpts.theState == {
assert theOpts._theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -129,7 +129,7 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.getValue("GuiBuildNovel", "mockItem", None) is None
# Get type-specific
assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100"
assert theOpts.getString("GuiBuildNovel", "winWidth", None) is None
assert theOpts.getString("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None
+1 -1
View File
@@ -61,7 +61,7 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
assert spChk._readProjectDictionary(None) is False
assert spChk._readProjectDictionary(wList) is True
assert spChk.projectDict == wList
assert spChk._projectDict == wList
# Cannot write to file
with monkeypatch.context() as mp:
+55 -55
View File
@@ -27,59 +27,6 @@ from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToHtml
@pytest.mark.core
def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Export Mode
# ===========
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='synopsis'><strong>Synopsis:</strong> synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'><strong>Comment:</strong> comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, "
"<a href='#tag_Jane'>Jane</a>"
)
# Preview Mode
# ============
theHtml.setPreview(True, True)
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'>comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<span class='tags'>Characters:</span> "
"<a href='#char=Bod'>Bod</a>, "
"<a href='#char=Jane'>Jane</a>"
)
# END Test testCoreToHtml_Format
@pytest.mark.core
def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class.
@@ -433,7 +380,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
@pytest.mark.core
def testCoreToHtml_Complex(mockGUI, fncDir):
"""Test the ave method of the ToHtml class.
"""Test the save method of the ToHtml class.
"""
theProject = NWProject(mockGUI)
theHtml = ToHtml(theProject)
@@ -524,7 +471,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
theHtml.saveHTML5(saveFile)
assert readFile(saveFile) == htmlDoc
# END Test testCoreToHtml_Save
# END Test testCoreToHtml_Complex
@pytest.mark.core
@@ -589,3 +536,56 @@ def testCoreToHtml_Methods(mockGUI):
assert theHtml.getStyleSheet() == []
# END Test testCoreToHtml_Methods
@pytest.mark.core
def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Export Mode
# ===========
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='synopsis'><strong>Synopsis:</strong> synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'><strong>Comment:</strong> comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, "
"<a href='#tag_Jane'>Jane</a>"
)
# Preview Mode
# ============
theHtml.setPreview(True, True)
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'>comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<span class='tags'>Characters:</span> "
"<a href='#char=Bod'>Bod</a>, "
"<a href='#char=Jane'>Jane</a>"
)
# END Test testCoreToHtml_Format
+277
View File
@@ -0,0 +1,277 @@
"""
novelWriter ToMd Class Tester
===============================
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
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 os
import pytest
from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToMarkdown
@pytest.mark.core
def testCoreToMarkdown_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
# Headers
# =======
theMD.isNovel = True
theMD.isNote = False
theMD.isFirst = True
# Header 1
theMD.theText = "# Partition\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "# Partition\n\n"
# Header 2
theMD.theText = "## Chapter Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "## Chapter Title\n\n"
# Header 3
theMD.theText = "### Scene Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "### Scene Title\n\n"
# Header 4
theMD.theText = "#### Section Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "#### Section Title\n\n"
# Title
theMD.theText = "#! Title\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "# Title\n\n"
# Unnumbered
theMD.theText = "##! Prologue\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "## Prologue\n\n"
# Paragraphs
# ==========
# Text for GitHub Markdown
theMD.setGitHubMarkdown()
theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
# Text for Standard Markdown
theMD.setStandardMarkdown()
theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
"Some **nested bold and _italic_ and strikethrough text** here\n\n"
)
# Text w/Hard Break
theMD.theText = "Line one \nLine two \nLine three\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "Line one \nLine two \nLine three\n\n"
# Synopsis
theMD.theText = "%synopsis: The synopsis ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setSynopsis(True)
theMD.theText = "%synopsis: The synopsis ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Synopsis:** The synopsis ...\n\n"
# Comment
theMD.theText = "% A comment ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setComments(True)
theMD.theText = "% A comment ...\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Comment:** A comment ...\n\n"
# Keywords
theMD.theText = "@char: Bod, Jane\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == ""
theMD.setKeywords(True)
theMD.theText = "@char: Bod, Jane\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == "**Characters:** Bod, Jane\n\n"
# Multiple Keywords
theMD.setKeywords(True)
theMD.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == (
"## Chapter\n\n"
"**Point of View:** Bod \n"
"**Plot:** Main \n"
"**Locations:** Europe\n\n"
)
# END Test testCoreToMarkdown_ConvertFormat
@pytest.mark.core
def testCoreToMarkdown_ConvertDirect(mockGUI):
"""Test the converter directly using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
theMD.isNovel = True
theMD.isNote = False
# Special Titles
# ==============
# Title
theMD.theTokens = [
(theMD.T_TITLE, 1, "A Title", None, theMD.A_PBB | theMD.A_CENTRE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
theMD.doConvert()
assert theMD.theResult == "# A Title\n\n"
# Unnumbered
theMD.theTokens = [
(theMD.T_UNNUM, 1, "Prologue", None, theMD.A_PBB),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
theMD.doConvert()
assert theMD.theResult == "## Prologue\n\n"
# Separators
# ==========
# Separator
theMD.theTokens = [
(theMD.T_SEP, 1, "* * *", None, theMD.A_CENTRE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
theMD.doConvert()
assert theMD.theResult == "* * *\n\n"
# Skip
theMD.theTokens = [
(theMD.T_SKIP, 1, "", None, theMD.A_NONE),
(theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
]
theMD.doConvert()
assert theMD.theResult == "\n\n\n"
# END Test testCoreToMarkdown_ConvertDirect
@pytest.mark.core
def testCoreToMarkdown_Complex(mockGUI, fncDir):
"""Test the save method of the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
theMD = ToMarkdown(theProject)
theMD.isNovel = True
# Build Project
# =============
docText = [
"# My Novel\n**By Jane Doh**\n",
"## Chapter 1\n\nThe text of chapter one.\n",
"### Scene 1\n\nThe text of scene one.\n",
"#### A Section\n\nMore text in scene one.\n",
"## Chapter 2\n\nThe text of chapter two.\n",
"### Scene 2\n\nThe text of scene two.\n",
"#### A Section\n\n\tMore text in scene two.\n",
]
resText = [
"# My Novel\n\n**By Jane Doh**\n\n",
"## Chapter 1\n\nThe text of chapter one.\n\n",
"### Scene 1\n\nThe text of scene one.\n\n",
"#### A Section\n\nMore text in scene one.\n\n",
"## Chapter 2\n\nThe text of chapter two.\n\n",
"### Scene 2\n\nThe text of scene two.\n\n",
"#### A Section\n\n\tMore text in scene two.\n\n",
]
for i in range(len(docText)):
theMD.theText = docText[i]
theMD.doPreProcessing()
theMD.tokenizeText()
theMD.doConvert()
assert theMD.theResult == resText[i]
assert theMD.fullMD == resText
assert theMD.getFullResultSize() == len("".join(resText))
theMD.replaceTabs(nSpaces=4, spaceChar=" ")
resText[6] = "#### A Section\n\n More text in scene two.\n\n"
# Check File
# ==========
saveFile = os.path.join(fncDir, "outFile.md")
theMD.saveMarkdown(saveFile)
assert readFile(saveFile) == "".join(resText)
# END Test testCoreToHtml_Complex
@pytest.mark.core
def testCoreToMarkdown_Format(mockGUI):
"""Test all the formatters for the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
assert theMD._formatKeywords("", theMD.A_NONE) == ""
assert theMD._formatKeywords("tag: Jane", theMD.A_NONE) == "**Tag:** Jane\n\n"
assert theMD._formatKeywords("tag: Jane, John", theMD.A_NONE) == "**Tag:** Jane, John\n\n"
assert theMD._formatKeywords("tag: Jane", theMD.A_Z_BTMMRG) == "**Tag:** Jane \n"
# END Test testCoreToMarkdown_Format
+86
View File
@@ -559,6 +559,62 @@ def testCoreToOdt_Convert(mockGUI):
# END Test testCoreToOdt_Convert
@pytest.mark.core
def testCoreToOdt_ConvertDirect(mockGUI):
"""Test the converter directly using the ToOdt class to reach some
otherwise hard to reach conditions.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.isNovel = True
# Justified
theDoc = ToOdt(theProject, isFlat=True)
theDoc.theTokens = [
(theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_JUSTIFY),
(theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
]
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert (
'<style:style style:name="P1" style:family="paragraph" '
'style:parent-style-name="Text_Body">'
'<style:paragraph-properties fo:text-align="justify"/>'
'</style:style>'
) in xmlToText(theDoc._xAuto)
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:p text:style-name="P1">This is a paragraph</text:p>'
'</office:text>'
)
# Page Break After
theDoc = ToOdt(theProject, isFlat=True)
theDoc.theTokens = [
(theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_PBA),
(theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
]
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert (
'<style:style style:name="P1" style:family="paragraph" '
'style:parent-style-name="Text_Body">'
'<style:paragraph-properties fo:break-after="page"/>'
'</style:style>'
) in xmlToText(theDoc._xAuto)
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:p text:style-name="P1">This is a paragraph</text:p>'
'</office:text>'
)
# END Test testCoreToOdt_ConvertDirect
@pytest.mark.core
def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
@@ -676,6 +732,36 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
# END Test testCoreToOdt_SaveFull
@pytest.mark.core
def testCoreToOdt_Format(mockGUI):
"""Test the formatters for the ToOdt class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
assert theDoc._formatSynopsis("synopsis text") == (
"**Synopsis:** synopsis text",
"_B b_ "
)
assert theDoc._formatComments("comment text") == (
"**Comment:** comment text",
"_B b_ "
)
assert theDoc._formatKeywords("") == ""
assert theDoc._formatKeywords("tag: Jane") == (
"**Tag:** Jane",
"_B b_ "
)
assert theDoc._formatKeywords("char: Bod, Jane") == (
"**Characters:** Bod, Jane",
"_B b_ "
)
# END Test testCoreToOdt_Format
@pytest.mark.core
def testCoreToOdt_ODTParagraphStyle():
"""Test the ODTParagraphStyle class.
+34 -18
View File
@@ -27,23 +27,22 @@ from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.dialogs import GuiAbout
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI):
"""Test the full about dialogs.
def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
"""Test the novelWriter about dialogs.
"""
# NW About
monkeypatch.setattr(GuiAbout, "exec_", lambda *a: None)
nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# NW About
nwGUI.theTheme.themeName = "A Theme"
nwGUI.theTheme.themeAuthor = "An Author"
assert nwGUI.showAboutNWDialog(showNotes=True) is True
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
msgAbout = getGuiItem("GuiAbout")
assert isinstance(msgAbout, GuiAbout)
msgAbout.show()
assert msgAbout.pageAbout.document().characterCount() > 100
assert msgAbout.pageNotes.document().characterCount() > 100
@@ -59,12 +58,29 @@ def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI):
msgAbout.showReleaseNotes()
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
# Qt About
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *a, **k: None)
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
# qtbot.stopForInteraction()
msgAbout._doClose()
# END Test testDlgAbout_Dialog
# Open Again from Menu
nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
msgAbout = getGuiItem("GuiAbout")
assert msgAbout is not None
msgAbout._doClose()
# END Test testDlgAbout_NWDialog
@pytest.mark.gui
def testDlgAbout_QtDialog(monkeypatch, nwGUI):
"""Test the Qt about dialogs.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *a, **k: None)
# Open About
# All it can do is check aigainst a crash
assert nwGUI.showAboutQtDialog() is True
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
# END Test testDlgAbout_QtDialog
@@ -31,10 +31,6 @@ from novelwriter.dialogs import GuiDocMerge, GuiItemEditor
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.tree import NWTree
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -96,7 +92,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwMerge = getGuiItem("GuiDocMerge")
assert isinstance(nwMerge, GuiDocMerge)
nwMerge.show()
qtbot.wait(stepDelay)
qtbot.wait(50)
# Populate List
# =============
@@ -32,10 +32,6 @@ from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.document import NWDoc
from novelwriter.core.tree import NWTree
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -103,7 +99,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit)
nwSplit.show()
qtbot.wait(stepDelay)
qtbot.wait(50)
# Populate List
# =============
@@ -253,7 +249,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit.sourceItem = None
assert nwSplit._doSplit() is False
# Close up
# Close
nwSplit._doClose()
# qtbot.stopForInteraction()
+175 -55
View File
@@ -20,93 +20,213 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
from shutil import copyfile
from tools import cmpFiles, getGuiItem
from tools import getGuiItem
from PyQt5.QtWidgets import QAction, QMessageBox
from PyQt5.QtWidgets import QAction, QDialog, QMessageBox
from novelwriter.gui import GuiProjectTree
from novelwriter.enum import nwItemLayout
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.dialogs import GuiItemEditor
keyDelay = 2
typeDelay = 1
stepDelay = 20
from novelwriter.core.tree import NWTree
@pytest.mark.gui
def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
"""Test the full item editor dialog.
def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
"""Test launching the item editor dialog from GuiMain.
"""
projFile = os.path.join(fncProj, "nwProject.nwx")
testFile = os.path.join(outDir, "guiItemEditor_Dialog_nwProject.nwx")
compFile = os.path.join(refDir, "guiItemEditor_Dialog_nwProject.nwx")
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# Block Dialog exec_
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
# Open Editor wo/Project
assert nwGUI.editItem() is False
# Create and Open Project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# No Selection
nwGUI.treeView.clearSelection()
assert nwGUI.editItem() is False
# Force opening from editor
assert nwGUI.openDocument("0e17daca5f3e1")
nwGUI.isFocusMode = True
# Block Tree Lookup
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
assert nwGUI.editItem() is False
# Invalid Type
nwGUI.theProject.projTree["0e17daca5f3e1"].itemType = nwItemType.NO_TYPE
assert nwGUI.editItem() is False
nwGUI.theProject.projTree["0e17daca5f3e1"].itemType = nwItemType.FILE
# Open Properly
assert nwGUI.editItem() is True
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert itemEdit is not None
itemEdit.close()
# Open Via Menu
with monkeypatch.context() as mp:
mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert itemEdit is not None
itemEdit.close()
nwGUI.isFocusMode = False
# END Test testDlgItemEditor_Dialog
@pytest.mark.gui
def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a novel document.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create new, save, open project
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.openDocument("0e17daca5f3e1")
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
# Check that an invalid handle is managed
itemEdit = GuiItemEditor(nwGUI, "whatever")
itemEdit.show()
itemEdit._doClose()
itemEdit = getGuiItem("GuiItemEditor")
assert isinstance(itemEdit, GuiItemEditor)
# Edit a Document
itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1")
itemEdit.show()
qtbot.addWidget(itemEdit)
# Check Existing Settings
assert itemEdit.editName.text() == "New Scene"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
assert itemEdit.editExport.isChecked() is True
for c in "Just a Page":
qtbot.keyClick(itemEdit.editName, c, delay=typeDelay)
# Change Settings
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE)
itemEdit.editName.setText("Great Scene")
itemEdit.editStatus.setCurrentIndex(1)
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.DOCUMENT)
itemEdit.editLayout.setCurrentIndex(layoutIdx)
itemEdit.editExport.setChecked(False)
assert not itemEdit.editExport.isChecked()
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Great Scene"
assert itemEdit.theItem.itemStatus == "Note"
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert isinstance(itemEdit, GuiItemEditor)
itemEdit.show()
qtbot.addWidget(itemEdit)
assert itemEdit.editName.text() == "Just a Page"
assert itemEdit.editStatus.currentData() == "Note"
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
itemEdit._doClose()
# Check that the header is updated
# Check that the editor header is updated
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Just a Page"
assert not nwGUI.docEditor.setCursorLine("where?")
assert nwGUI.docEditor.setCursorLine(2)
qtbot.wait(stepDelay)
assert nwGUI.docEditor.getCursorPosition() == 15
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
qtbot.wait(stepDelay)
# Check the files
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Great Scene"
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Dialog
@pytest.mark.gui
def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a project note.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# Create Note
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
# Open Note
assert nwGUI.openDocument("1a6562590ef19")
# Edit a Document
itemEdit = GuiItemEditor(nwGUI, "1a6562590ef19")
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New File"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True
# Change Settings
itemEdit.editName.setText("New Character")
itemEdit.editStatus.setCurrentIndex(1)
itemEdit.editExport.setChecked(False)
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "New Character"
assert itemEdit.theItem.itemStatus == "Minor"
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Note
@pytest.mark.gui
def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a folder.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# Edit a Folder
itemEdit = GuiItemEditor(nwGUI, "31489056e0916")
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New Chapter"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT
assert itemEdit.editExport.isChecked() is False
assert itemEdit.editLayout.isEnabled() is False
assert itemEdit.editExport.isEnabled() is False
# Change Settings
itemEdit.editName.setText("Chapter One")
itemEdit.editStatus.setCurrentIndex(1)
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Chapter One"
assert itemEdit.theItem.itemStatus == "Note"
assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT
assert itemEdit.theItem.isExported is False
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Folder