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** **Variable and Function Names**
* PEP8 allows for camelCase for consistency with existing code. The Qt library uses camelCase, so * 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 * The exception to the above is for constants. They should always be in upper snake case, like PEP8
states. states.
**Spaces, Indentation and Alignment** **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. * 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 * Ideally, a function should end on the same indention level as it started. Exceptions are allowed
if it makes the code easier to follow. if it makes the code easier to follow.
+10 -10
View File
@@ -117,7 +117,7 @@ def main(sysArgs=None):
# Valid Input Options # Valid Input Options
shortOpt = "hv" shortOpt = "hv"
longOpt = [ longOpt = [
"help", "help",
"version", "version",
"info", "info",
@@ -215,31 +215,31 @@ def main(sysArgs=None):
errorCode = 0 errorCode = 0
if sys.hexversion < 0x030600f0: if sys.hexversion < 0x030600f0:
errorData.append( 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 errorCode |= 4
if CONFIG.verQtValue < 50300: if CONFIG.verQtValue < 50300:
errorData.append( 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 errorCode |= 8
if CONFIG.verPyQtValue < 50300: if CONFIG.verPyQtValue < 50300:
errorData.append( 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 errorCode |= 16
try: try:
import lxml # noqa: F401 import lxml # noqa: F401
except ImportError: except ImportError:
errorData.append("Python module 'lxml' is missing.") errorData.append("Python module 'lxml' is missing")
errorCode |= 32 errorCode |= 32
if errorData: if errorData:
errApp = QApplication([]) errApp = QApplication([])
errMsg = QErrorMessage() errDlg = QErrorMessage()
errMsg.resize(500, 300) errDlg.resize(500, 300)
errMsg.showMessage(( errDlg.showMessage((
"<h3>A critical error has been encountered</h3>" "<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>" "<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>" "<p>&nbsp;-&nbsp;%s</p>"
@@ -247,8 +247,8 @@ def main(sysArgs=None):
) % ( ) % (
"<br>&nbsp;-&nbsp;".join(errorData) "<br>&nbsp;-&nbsp;".join(errorData)
)) ))
for errMsg in errorData: for errLine in errorData:
logger.critical(errMsg) logger.critical(errLine)
errApp.exec_() errApp.exec_()
sys.exit(errorCode) sys.exit(errorCode)
+24 -13
View File
@@ -46,7 +46,7 @@ logger = logging.getLogger(__name__)
# =============================================================================================== # # =============================================================================================== #
def checkString(value, default, allowNone=False): 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"): if allowNone and (value is None or value == "None"):
return None return None
@@ -56,7 +56,7 @@ def checkString(value, default, allowNone=False):
def checkInt(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"): if allowNone and (value is None or value == "None"):
return None return None
@@ -66,8 +66,19 @@ def checkInt(value, default, allowNone=False):
return default 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): 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"): if allowNone and (value is None or value == "None"):
return None return None
@@ -211,7 +222,7 @@ def formatTime(tS):
def parseTimeStamp(theStamp, default, allowNone=False): 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. a float. Note that negative timestamps cause an OSError on Windows.
See https://bugs.python.org/issue29097 See https://bugs.python.org/issue29097
""" """
@@ -228,7 +239,7 @@ def parseTimeStamp(theStamp, default, allowNone=False):
# =============================================================================================== # # =============================================================================================== #
def splitVersionNumber(value): 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. and patch, and computes an integer value aabbcc.
""" """
if not isinstance(value, str): if not isinstance(value, str):
@@ -337,7 +348,7 @@ def fuzzyTime(secDiff):
).format(int(round(secDiff/31557600))) ).format(int(round(secDiff/31557600)))
def numberToRoman(numVal, isLower=False): def numberToRoman(numVal, toLower=False):
"""Convert an integer to a Roman number. """Convert an integer to a Roman number.
""" """
if not isinstance(numVal, int): if not isinstance(numVal, int):
@@ -358,7 +369,7 @@ def numberToRoman(numVal, isLower=False):
if numVal <= 0: if numVal <= 0:
break break
return romNum.lower() if isLower else romNum return romNum.lower() if toLower else romNum
# =============================================================================================== # # =============================================================================================== #
@@ -434,11 +445,11 @@ def readTextFile(filePath):
return fileText return fileText
def makeFileNameSafe(theText): def makeFileNameSafe(value):
"""Returns a filename safe version of the text. """Returns a filename safe string of the value.
""" """
cleanName = "" cleanName = ""
for c in theText.strip(): for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ": if c.isalpha() or c.isdigit() or c == " ":
cleanName += c cleanName += c
return cleanName return cleanName
@@ -456,7 +467,7 @@ def sha256sum(filePath):
for n in iter(lambda: inFile.readinto(mData), 0): for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n]) hDigest.update(mData[:n])
except Exception: except Exception:
logger.error("Could not read sha256sum of: %s", filePath) logger.error("Could not create sha256sum of: %s", filePath)
logException() logException()
return None return None
@@ -467,11 +478,11 @@ def sha256sum(filePath):
# Other Functions # Other Functions
# =============================================================================================== # # =============================================================================================== #
def getGuiItem(theName): def getGuiItem(objName):
"""Returns a QtWidget based on its objectName. """Returns a QtWidget based on its objectName.
""" """
for qWidget in qApp.topLevelWidgets(): for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName: if qWidget.objectName() == objName:
return qWidget return qWidget
return None return None
+65 -30
View File
@@ -302,7 +302,7 @@ class Config:
logger.verbose("App path: %s", self.appPath) logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath) 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. # This assumes that the os config folder itself exists.
if not os.path.isdir(self.confPath): if not os.path.isdir(self.confPath):
try: try:
@@ -324,7 +324,7 @@ class Config:
# If it does not exist, save a copy of the default values # If it does not exist, save a copy of the default values
self.saveConfig() 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. # This assumes that the os data folder itself exists.
if self.dataPath is not None: if self.dataPath is not None:
if not os.path.isdir(self.dataPath): if not os.path.isdir(self.dataPath):
@@ -534,7 +534,7 @@ class Config:
logger.info("Using straight single quotes, so disabling auto-replace") logger.info("Using straight single quotes, so disabling auto-replace")
self.doReplaceSQuote = False 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") logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False self.doReplaceDQuote = False
@@ -671,36 +671,28 @@ class Config:
if self.dataPath is None: if self.dataPath is None:
return False return False
cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {} self.recentProj = {}
if os.path.isfile(cacheFile): cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
try: if not os.path.isfile(cacheFile):
with open(cacheFile, mode="r", encoding="utf-8") as inFile: return True
theData = json.load(inFile)
for projPath in theData.keys(): try:
theEntry = theData[projPath] with open(cacheFile, mode="r", encoding="utf-8") as inFile:
theTitle = "" theData = json.load(inFile)
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,
}
except Exception as e: for projPath, theEntry in theData.items():
self.hasError = True self.recentProj[projPath] = {
self.errData.append("Could not load recent project cache") "title": theEntry.get("title", ""),
self.errData.append(str(e)) "time": theEntry.get("time", 0),
return False "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 return True
@@ -755,6 +747,8 @@ class Config:
## ##
def setConfPath(self, newPath): def setConfPath(self, newPath):
"""Set the path and filename to the config file.
"""
if newPath is None: if newPath is None:
return True return True
if not os.path.isfile(newPath): if not os.path.isfile(newPath):
@@ -765,6 +759,8 @@ class Config:
return True return True
def setDataPath(self, newPath): def setDataPath(self, newPath):
"""Set the data path.
"""
if newPath is None: if newPath is None:
return True return True
if not os.path.isdir(newPath): if not os.path.isdir(newPath):
@@ -774,6 +770,8 @@ class Config:
return True return True
def setLastPath(self, lastPath): def setLastPath(self, lastPath):
"""Set the last used path (by the user).
"""
if lastPath is None or lastPath == "": if lastPath is None or lastPath == "":
self.lastPath = "" self.lastPath = ""
else: else:
@@ -781,6 +779,11 @@ class Config:
return True return True
def setWinSize(self, newWidth, newHeight): 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) newWidth = int(newWidth/self.guiScale)
newHeight = int(newHeight/self.guiScale) newHeight = int(newHeight/self.guiScale)
if abs(self.winGeometry[0] - newWidth) > 5: if abs(self.winGeometry[0] - newWidth) > 5:
@@ -792,57 +795,79 @@ class Config:
return True return True
def setPreferencesSize(self, newWidth, newHeight): def setPreferencesSize(self, newWidth, newHeight):
"""Sat the size of the Preferences dialog window.
"""
self.prefGeometry[0] = int(newWidth/self.guiScale) self.prefGeometry[0] = int(newWidth/self.guiScale)
self.prefGeometry[1] = int(newHeight/self.guiScale) self.prefGeometry[1] = int(newHeight/self.guiScale)
self.confChanged = True self.confChanged = True
return True return True
def setTreeColWidths(self, colWidths): def setTreeColWidths(self, colWidths):
"""Set the column widths of the main project tree.
"""
self.treeColWidth = [int(x/self.guiScale) for x in colWidths] self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True self.confChanged = True
return True return True
def setNovelColWidths(self, colWidths): def setNovelColWidths(self, colWidths):
"""Set the column widths of the novel tree.
"""
self.novelColWidth = [int(x/self.guiScale) for x in colWidths] self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True self.confChanged = True
return True return True
def setProjColWidths(self, colWidths): def setProjColWidths(self, colWidths):
"""Set the column widths of the Load Project dialog.
"""
self.projColWidth = [int(x/self.guiScale) for x in colWidths] self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True self.confChanged = True
return True return True
def setMainPanePos(self, panePos): def setMainPanePos(self, panePos):
"""Set the position of the main GUI splitter.
"""
self.mainPanePos = [int(x/self.guiScale) for x in panePos] self.mainPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return True
def setDocPanePos(self, panePos): def setDocPanePos(self, panePos):
"""Set the position of the main editor/viewer splitter.
"""
self.docPanePos = [int(x/self.guiScale) for x in panePos] self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return True
def setViewPanePos(self, panePos): def setViewPanePos(self, panePos):
"""Set the position of the viewer meta data splitter.
"""
self.viewPanePos = [int(x/self.guiScale) for x in panePos] self.viewPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return True
def setOutlinePanePos(self, panePos): def setOutlinePanePos(self, panePos):
"""Set the position of the outline details splitter.
"""
self.outlnPanePos = [int(x/self.guiScale) for x in panePos] self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return True
def setShowRefPanel(self, checkState): def setShowRefPanel(self, checkState):
"""Set the visibility state of the reference panel.
"""
self.showRefPanel = checkState self.showRefPanel = checkState
self.confChanged = True self.confChanged = True
return self.showRefPanel return self.showRefPanel
def setViewComments(self, viewState): def setViewComments(self, viewState):
"""Set the visibility state of comments in the viewer.
"""
self.viewComments = viewState self.viewComments = viewState
self.confChanged = True self.confChanged = True
return self.viewComments return self.viewComments
def setViewSynopsis(self, viewState): def setViewSynopsis(self, viewState):
"""Set the visibility state of synopsis comments in the viewer.
"""
self.viewSynopsis = viewState self.viewSynopsis = viewState
self.confChanged = True self.confChanged = True
return self.viewSynopsis return self.viewSynopsis
@@ -852,12 +877,18 @@ class Config:
## ##
def setDefaultGuiTheme(self): def setDefaultGuiTheme(self):
"""Reset the GUI theme to default value.
"""
self.guiTheme = "default" self.guiTheme = "default"
def setDefaultSyntaxTheme(self): def setDefaultSyntaxTheme(self):
"""Reset the syntax theme to default value.
"""
self.guiSyntax = "default_light" self.guiSyntax = "default_light"
def setDefaultIconTheme(self): def setDefaultIconTheme(self):
"""Reset the icon theme to default value.
"""
self.guiIcons = "typicons_light" self.guiIcons = "typicons_light"
## ##
@@ -904,6 +935,9 @@ class Config:
return self.pxInt(self.focusWidth) return self.pxInt(self.focusWidth)
def getErrData(self): 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) errMessage = "<br>".join(self.errData)
self.hasError = False self.hasError = False
self.errData = [] self.errData = []
@@ -914,7 +948,8 @@ class Config:
## ##
def _packList(self, inData): 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]) return ", ".join([str(inVal) for inVal in inData])
+8 -11
View File
@@ -287,10 +287,8 @@ class NWIndex():
nLine = 0 nLine = 0
nTitle = 0 nTitle = 0
theLines = theText.splitlines() theLines = theText.splitlines()
for aLine in theLines: for nLine, aLine in enumerate(theLines, start=1):
nLine += 1 if len(aLine.strip()) == 0:
nChar = len(aLine.strip())
if nChar == 0:
continue continue
if aLine.startswith("#"): if aLine.startswith("#"):
@@ -363,7 +361,7 @@ class NWIndex():
else: else:
return False return False
sTitle = "T%06d" % nLine sTitle = f"T{nLine:06d}"
self._fileIndex[tHandle][sTitle] = { self._fileIndex[tHandle][sTitle] = {
"level": hDepth, "level": hDepth,
"title": hText, "title": hText,
@@ -391,14 +389,13 @@ class NWIndex():
"pCount": 0, "pCount": 0,
"synopsis": "", "synopsis": "",
} }
return return
def _indexWordCounts(self, tHandle, theText, nTitle): def _indexWordCounts(self, tHandle, theText, nTitle):
"""Count text stats and save the counts to the index. """Count text stats and save the counts to the index.
""" """
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
sTitle = "T%06d" % nTitle sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex: if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]: if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["cCount"] = cC self._fileIndex[tHandle][sTitle]["cCount"] = cC
@@ -409,7 +406,7 @@ class NWIndex():
def _indexSynopsis(self, tHandle, theText, nTitle): def _indexSynopsis(self, tHandle, theText, nTitle):
"""Save the synopsis to the index. """Save the synopsis to the index.
""" """
sTitle = "T%06d" % nTitle sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex: if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]: if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["synopsis"] = theText self._fileIndex[tHandle][sTitle]["synopsis"] = theText
@@ -428,7 +425,7 @@ class NWIndex():
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle) logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return return
sTitle = "T%06d" % nTitle sTitle = f"T{nTitle:06d}"
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
@@ -525,7 +522,7 @@ class NWIndex():
""" """
for tHandle in self._listNovelHandles(skipExcluded): for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._fileIndex[tHandle]): for sTitle in sorted(self._fileIndex[tHandle]):
tKey = "%s:%s" % (tHandle, sTitle) tKey = f"{tHandle}:{sTitle}"
yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle] yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle]
def getNovelWordCount(self, skipExcluded=True): def getNovelWordCount(self, skipExcluded=True):
@@ -559,7 +556,7 @@ class NWIndex():
return theCounts return theCounts
for sTitle, sData in hRecord.items(): for sTitle, sData in hRecord.items():
theCounts.append(("%s:%s" % (tHandle, sTitle), sData["wCount"])) theCounts.append((f"{tHandle}:{sTitle}", sData["wCount"]))
return theCounts return theCounts
+6 -15
View File
@@ -103,11 +103,8 @@ class NWItem():
logger.error("XML item entry does not have a handle") logger.error("XML item entry does not have a handle")
return False return False
if "parent" in xItem.attrib: self.setParent(xItem.attrib.get("parent", None))
self.setParent(xItem.attrib["parent"]) self.setOrder(xItem.attrib.get("order", 0))
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
tmpStatus = "" tmpStatus = ""
for xValue in xItem: for xValue in xItem:
@@ -200,11 +197,8 @@ class NWItem():
def setHandle(self, theHandle): def setHandle(self, theHandle):
"""Set the item handle, and ensure it is valid. """Set the item handle, and ensure it is valid.
""" """
if isinstance(theHandle, str): if isHandle(theHandle):
if isHandle(theHandle): self.itemHandle = theHandle
self.itemHandle = theHandle
else:
self.itemHandle = None
else: else:
self.itemHandle = None self.itemHandle = None
return return
@@ -214,11 +208,8 @@ class NWItem():
""" """
if theParent is None: if theParent is None:
self.itemParent = None self.itemParent = None
elif isinstance(theParent, str): elif isHandle(theParent):
if isHandle(theParent): self.itemParent = theParent
self.itemParent = theParent
else:
self.itemParent = None
else: else:
self.itemParent = None self.itemParent = None
return return
+70 -130
View File
@@ -30,88 +30,40 @@ import logging
import novelwriter import novelwriter
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
logger = logging.getLogger(__name__) 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(): class OptionState():
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
self.theState = {} 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",
}
}
return return
## ##
@@ -139,11 +91,11 @@ class OptionState():
# Filter out unused variables # Filter out unused variables
for aGroup in theState: for aGroup in theState:
if aGroup in self.validMap: if aGroup in VALID_MAP:
self.theState[aGroup] = {} self._theState[aGroup] = {}
for anOpt in theState[aGroup]: for anOpt in theState[aGroup]:
if anOpt in self.validMap[aGroup]: if anOpt in VALID_MAP[aGroup]:
self.theState[aGroup][anOpt] = theState[aGroup][anOpt] self._theState[aGroup][anOpt] = theState[aGroup][anOpt]
return True return True
@@ -158,7 +110,7 @@ class OptionState():
try: try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile: 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: except Exception:
logger.error("Failed to save GUI options file") logger.error("Failed to save GUI options file")
novelwriter.logException() novelwriter.logException()
@@ -170,21 +122,21 @@ class OptionState():
# Setters # Setters
## ##
def setValue(self, setGroup, setName, setValue): def setValue(self, group, name, value):
"""Saves a value, with a given group and name. """Saves a value, with a given group and name.
""" """
if setGroup not in self.validMap: if group not in VALID_MAP:
logger.error("Unknown option group '%s'", setGroup) logger.error("Unknown option group '%s'", group)
return False return False
if setName not in self.validMap[setGroup]: if name not in VALID_MAP[group]:
logger.error("Unknown option name '%s'", setName) logger.error("Unknown option name '%s'", name)
return False return False
if setGroup not in self.theState: if group not in self._theState:
self.theState[setGroup] = {} self._theState[group] = {}
self.theState[setGroup][setName] = setValue self._theState[group][name] = value
return True return True
@@ -192,79 +144,67 @@ class OptionState():
# Getters # Getters
## ##
def getValue(self, getGroup, getName, defaultValue): def getValue(self, group, name, default):
"""Return an arbitrary type value, if it exists. Otherwise, """Return an arbitrary type value, if it exists. Otherwise,
return the default value. return the default value.
""" """
if getGroup in self.theState: if group in self._theState:
if getName in self.theState[getGroup]: return self._theState[group].get(name, default)
return self.theState[getGroup][getName] return default
return defaultValue
def getString(self, getGroup, getName, defaultValue): def getString(self, group, name, default):
"""Return the value as a string, if it exists. Otherwise, return """Return the value as a string, if it exists. Otherwise, return
the default value. the default value.
""" """
if getGroup in self.theState: if group in self._theState:
if getName in self.theState[getGroup]: return checkString(self._theState[group].get(name, default), default)
return str(self.theState[getGroup][getName]) return default
return defaultValue
def getInt(self, getGroup, getName, defaultValue): def getInt(self, group, name, default):
"""Return the value as an int, if it exists. Otherwise, return """Return the value as an int, if it exists. Otherwise, return
the default value. the default value.
""" """
if getGroup in self.theState: if group in self._theState:
if getName in self.theState[getGroup]: return checkInt(self._theState[group].get(name, default), default)
try: return default
return int(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
def getFloat(self, getGroup, getName, defaultValue): def getFloat(self, group, name, default):
"""Return the value as a float, if it exists. Otherwise, return """Return the value as a float, if it exists. Otherwise, return
the default value. the default value.
""" """
if getGroup in self.theState: if group in self._theState:
if getName in self.theState[getGroup]: return checkFloat(self._theState[group].get(name, default), default)
try: return default
return float(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return defaultValue
def getBool(self, getGroup, getName, defaultValue): def getBool(self, group, name, default):
"""Return the value as a bool, if it exists. Otherwise, return """Return the value as a bool, if it exists. Otherwise, return
the default value. the default value.
""" """
if getGroup in self.theState: if group in self._theState:
if getName in self.theState[getGroup]: if name in self._theState[group]:
return bool(self.theState[getGroup][getName]) return checkBool(self._theState[group].get(name, default), default)
return defaultValue return default
## ##
# Validators # 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 """Check that an int is in a given range. If it isn't, return
the default value. the default value.
""" """
if isinstance(theValue, int): if isinstance(value, int):
if theValue >= intA and theValue <= intB: if value >= first and value <= last:
return theValue return value
return intDefault 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, """Check that an int is an element of a tuple. If it isn't,
return the default value. return the default value.
""" """
if isinstance(theValue, int): if isinstance(value, int):
if theValue in theTuple: if value in valid:
return theValue return value
return intDefault return default
# END Class OptionState # END Class OptionState
+9 -8
View File
@@ -125,6 +125,7 @@ class NWProject():
if not self.projTree.checkRootUnique(rootClass): if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR) self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None return None
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(rootName) newItem.setName(rootName)
newItem.setType(nwItemType.ROOT) newItem.setType(nwItemType.ROOT)
@@ -356,7 +357,7 @@ class NWProject():
return True return True
def openProject(self, fileName, overrideLock=False): 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, 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 parse the XML of the file and populate the project variables and
build the tree of project items. build the tree of project items.
@@ -469,8 +470,8 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to # parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10. # read the file. Introduced in version 0.10.
# 1.3 : Reduces the number of layouts to onlye two. One for # 1.3 : Reduces the number of layouts to onlye two. One for
# novel documents and one for notes. Introduced in version # novel documents and one for project notes. Introduced in
# 1.5. # version 1.5.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"): if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
@@ -973,7 +974,7 @@ class NWProject():
return False return False
self.bookAuthors = [] self.bookAuthors = []
for bookAuthor in bookAuthors.split("\n"): for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip() bookAuthor = bookAuthor.strip()
if bookAuthor == "": if bookAuthor == "":
continue continue
@@ -1074,7 +1075,7 @@ class NWProject():
replaceMap = self.statusItems.setNewEntries(newCols) replaceMap = self.statusItems.setNewEntries(newCols)
for nwItem in self.projTree: for nwItem in self.projTree:
if nwItem.itemClass == nwItemClass.NOVEL: if nwItem.itemClass == nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys(): if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus]) nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -1086,7 +1087,7 @@ class NWProject():
replaceMap = self.importItems.setNewEntries(newCols) replaceMap = self.importItems.setNewEntries(newCols)
for nwItem in self.projTree: for nwItem in self.projTree:
if nwItem.itemClass != nwItemClass.NOVEL: if nwItem.itemClass != nwItemClass.NOVEL:
if nwItem.itemStatus in replaceMap.keys(): if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus]) nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -1104,7 +1105,7 @@ class NWProject():
""" """
for valKey, valEntry in titleFormat.items(): for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat: if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False) self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
return True return True
def setProjectChanged(self, bValue): def setProjectChanged(self, bValue):
@@ -1346,7 +1347,7 @@ class NWProject():
return return
def _packProjectKeyValue(self, xParent, theName, theDict): 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) xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items(): for aKey, aValue in theDict.items():
+40 -28
View File
@@ -35,16 +35,24 @@ class NWSpellEnchant():
def __init__(self): def __init__(self):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theDict = None
self.projDict = set() self._theDict = None
self.projectDict = None self._projDict = set()
self.spellLanguage = None self._projectDict = None
self.theBroker = None self._spellLanguage = None
self._theBroker = None
logger.debug("Enchant spell checking activated") logger.debug("Enchant spell checking activated")
return return
##
# Getters and Setters
##
def spellLanguage(self):
return self._spellLanguage
def setLanguage(self, theLang, projectDict=None): def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config. """Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't If that fails, we load a mock dictionary so that lookups don't
@@ -52,49 +60,53 @@ class NWSpellEnchant():
""" """
try: try:
import enchant import enchant
if self.theBroker is not None: if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker") logger.debug("Deleting old pyenchant broker")
del self.theBroker del self._theBroker
self.theBroker = enchant.Broker() self._theBroker = enchant.Broker()
self.theDict = self.theBroker.request_dict(theLang) self._theDict = self._theBroker.request_dict(theLang)
self.spellLanguage = theLang self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang) logger.debug("Enchant spell checking for language '%s' loaded", theLang)
except Exception: except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang) logger.error("Failed to load enchant spell checking for language '%s'", theLang)
self.theDict = FakeEnchant() self._theDict = FakeEnchant()
self.spellLanguage = None self._spellLanguage = None
self._readProjectDictionary(projectDict) self._readProjectDictionary(projectDict)
for pWord in self.projDict: for pWord in self._projDict:
self.theDict.add_to_session(pWord) self._theDict.add_to_session(pWord)
return return
##
# Methods
##
def checkWord(self, theWord): def checkWord(self, theWord):
"""Wrapper function for pyenchant. """Wrapper function for pyenchant.
""" """
return self.theDict.check(theWord) return self._theDict.check(theWord)
def suggestWords(self, theWord): def suggestWords(self, theWord):
"""Wrapper function for pyenchant. """Wrapper function for pyenchant.
""" """
return self.theDict.suggest(theWord) return self._theDict.suggest(theWord)
def addWord(self, newWord): def addWord(self, newWord):
"""Add a word to the project dictionary. """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() newWord = newWord.strip()
try: 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) outFile.write("%s\n" % newWord)
self.projDict.add(newWord) self._projDict.add(newWord)
except Exception: 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() novelwriter.logException()
return False return False
return True return True
@@ -119,8 +131,8 @@ class NWSpellEnchant():
dictionary. dictionary.
""" """
try: try:
spTag = self.theDict.tag spTag = self._theDict.tag
spName = self.theDict.provider.name spName = self._theDict.provider.name
except Exception: except Exception:
logger.error("Failed to extract information about the dictionary") logger.error("Failed to extract information about the dictionary")
novelwriter.logException() novelwriter.logException()
@@ -137,8 +149,8 @@ class NWSpellEnchant():
"""Read the content of the project dictionary, and add it to the """Read the content of the project dictionary, and add it to the
lookup lists. lookup lists.
""" """
self.projDict = set() self._projDict = set()
self.projectDict = projectDict self._projectDict = projectDict
if projectDict is None: if projectDict is None:
return False return False
@@ -151,9 +163,9 @@ class NWSpellEnchant():
with open(projectDict, mode="r", encoding="utf-8") as wordsFile: with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile: for theLine in wordsFile:
theLine = theLine.strip() theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.projDict: if len(theLine) > 0 and theLine not in self._projDict:
self.projDict.add(theLine) self._projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self.projDict)) logger.debug("Project word list contains %d words", len(self._projDict))
except Exception: except Exception:
logger.error("Failed to load project word list") logger.error("Failed to load project word list")
+59 -63
View File
@@ -109,7 +109,7 @@ class ToHtml(Tokenizer):
to theResult. to theResult.
""" """
if self.genMode == self.M_PREVIEW: if self.genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2 htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "<b>", self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>", self.FMT_B_E: "</b>",
self.FMT_I_B: "<i>", self.FMT_I_B: "<i>",
@@ -118,7 +118,7 @@ class ToHtml(Tokenizer):
self.FMT_D_E: "</span>", self.FMT_D_E: "</span>",
} }
else: else:
htmlTags = { # HTML5 htmlTags = { # HTML5 (for export)
self.FMT_B_B: "<strong>", self.FMT_B_B: "<strong>",
self.FMT_B_E: "</strong>", self.FMT_B_E: "</strong>",
self.FMT_I_B: "<em>", self.FMT_I_B: "<em>",
@@ -176,17 +176,18 @@ class ToHtml(Tokenizer):
aStyle.append("margin-top: 0;") aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L: 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: 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: if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle)) stVals = " ".join(aStyle)
hStyle = f" style='{stVals}'"
else: else:
hStyle = "" hStyle = ""
if self.linkHeaders: if self.linkHeaders:
aNm = "<a name='T%06d'></a>" % tLine aNm = f"<a name='T{tLine:06d}'></a>"
else: else:
aNm = "" aNm = ""
@@ -200,36 +201,36 @@ class ToHtml(Tokenizer):
parClass = "" parClass = ""
if len(thisPar) > 0: if len(thisPar) > 0:
tTemp = "<br/>".join(thisPar) 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 = [] thisPar = []
parStyle = None parStyle = None
elif tType == self.T_TITLE: elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>") 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: elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "<br/>") 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: elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>") 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: elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>") 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: elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>") 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: elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>") 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: 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: elif tType == self.T_SKIP:
tmpResult.append("<p class='skip'>&nbsp;</p>\n") tmpResult.append("<p class='skip'>&nbsp;</p>\n")
@@ -249,7 +250,7 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText)) tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords: 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) tmpResult.append(tTemp)
self.theResult = "".join(tmpResult) self.theResult = "".join(tmpResult)
@@ -298,9 +299,9 @@ class ToHtml(Tokenizer):
"""Replace tabs with spaces in the html. """Replace tabs with spaces in the html.
""" """
htmlText = [] htmlText = []
eightSpace = spaceChar*nSpaces tabSpace = spaceChar*nSpaces
for aLine in self.fullHTML: for aLine in self.fullHTML:
htmlText.append(aLine.replace("\t", eightSpace)) htmlText.append(aLine.replace("\t", tabSpace))
self.fullHTML = htmlText self.fullHTML = htmlText
return return
@@ -315,75 +316,76 @@ class ToHtml(Tokenizer):
mScale = self.lineHeight/1.15 mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left" 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 self.textFont, self.textSize
)) ))
theStyles.append(( theStyles.append((
"p {" "p {{"
"text-align: %s; line-height: %d%%; " "text-align: {0}; line-height: {1:d}%; "
"margin-top: %.2fem; margin-bottom: %.2fem;" "margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}" "}}"
) % ( ).format(
textAlign, textAlign,
round(100 * self.lineHeight), round(100 * self.lineHeight),
mScale * self.marginText[0], mScale * self.marginText[0],
mScale * self.marginText[1], mScale * self.marginText[1],
)) ))
theStyles.append(( theStyles.append((
"h1 {" "h1 {{"
"color: rgb(66, 113, 174); " "color: rgb(66, 113, 174); "
"page-break-after: avoid; " "page-break-after: avoid; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;" "margin-bottom: {1:.2f}em;"
"}" "}}"
) % ( ).format(
mScale * self.marginHead1[0], mScale * self.marginHead1[1] mScale * self.marginHead1[0], mScale * self.marginHead1[1]
)) ))
theStyles.append(( theStyles.append((
"h2 {" "h2 {{"
"color: rgb(66, 113, 174); " "color: rgb(66, 113, 174); "
"page-break-after: avoid; " "page-break-after: avoid; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;" "margin-bottom: {1:.2f}em;"
"}" "}}"
) % ( ).format(
mScale * self.marginHead2[0], mScale * self.marginHead2[1] mScale * self.marginHead2[0], mScale * self.marginHead2[1]
)) ))
theStyles.append(( theStyles.append((
"h3 {" "h3 {{"
"color: rgb(50, 50, 50); " "color: rgb(50, 50, 50); "
"page-break-after: avoid; " "page-break-after: avoid; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;" "margin-bottom: {1:.2f}em;"
"}" "}}"
) % ( ).format(
mScale * self.marginHead3[0], mScale * self.marginHead3[1] mScale * self.marginHead3[0], mScale * self.marginHead3[1]
)) ))
theStyles.append(( theStyles.append((
"h4 {" "h4 {{"
"color: rgb(50, 50, 50); " "color: rgb(50, 50, 50); "
"page-break-after: avoid; " "page-break-after: avoid; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;" "margin-bottom: {1:.2f}em;"
"}" "}}"
) % ( ).format(
mScale * self.marginHead4[0], mScale * self.marginHead4[1] mScale * self.marginHead4[0], mScale * self.marginHead4[1]
)) ))
theStyles.append(( theStyles.append((
".title {" ".title {{"
"font-size: 2.5em; " "font-size: 2.5em; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;" "margin-bottom: {1:.2f}em;"
"}" "}}"
) % ( ).format(
mScale * self.marginTitle[0], mScale * self.marginTitle[1] mScale * self.marginTitle[0], mScale * self.marginTitle[1]
)) ))
theStyles.append(( theStyles.append((
".sep, .skip {" ".sep, .skip {{"
"text-align: center; " "text-align: center; "
"margin-top: %.2fem; " "margin-top: {0:.2f}em; "
"margin-bottom: %.2fem;}" "margin-bottom: {1:.2f}em;"
) % ( "}}"
).format(
mScale, mScale mScale, mScale
)) ))
@@ -421,31 +423,25 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText): def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords. """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: if not isValid or not theBits:
return "" return ""
retText = "" retText = ""
refTags = [] refTags = []
if theBits[0] in nwLabels.KEY_NAME: 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 len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'>%s</a>" % ( retText += f"<a name='tag_{theBits[1]}'>{theBits[1]}</a>"
theBits[1], theBits[1]
)
else: else:
if self.genMode == self.M_PREVIEW: if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]: for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % ( refTags.append(f"<a href='#{theBits[0][1:]}={tTag}'>{tTag}</a>")
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags) retText += ", ".join(refTags)
else: else:
for tTag in theBits[1:]: for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % ( refTags.append(f"<a href='#tag_{tTag}'>{tTag}</a>")
tTag, tTag
))
retText += ", ".join(refTags) retText += ", ".join(refTags)
return retText return retText
+5 -4
View File
@@ -267,13 +267,14 @@ class Tokenizer():
else: else:
textAlign = self.A_PBB | self.A_CENTRE 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 = []
self.theTokens.append(( self.theTokens.append((
self.T_TITLE, 0, theTitle, None, textAlign self.T_TITLE, 0, theTitle, None, textAlign
)) ))
if self.keepMarkdown: if self.keepMarkdown:
self.theMarkdown.append("# %s\n\n" % theTitle) self.theMarkdown.append(f"# {theTitle}\n\n")
return True return True
@@ -302,7 +303,7 @@ class Tokenizer():
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format( errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}" 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.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
@@ -318,7 +319,7 @@ class Tokenizer():
if len(self.theProject.autoReplace) > 0: if len(self.theProject.autoReplace) > 0:
repDict = {} repDict = {}
for aKey, aVal in self.theProject.autoReplace.items(): 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) 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) 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 # Process Text Type
if tType == self.T_EMPTY: if tType == self.T_EMPTY:
if len(thisPar) > 0: if len(thisPar) > 0:
tTemp = " \n".join(thisPar) tTemp = (" \n".join(thisPar)).rstrip(" ")
tmpResult.append("%s\n\n" % tTemp.rstrip(" ")) tmpResult.append(f"{tTemp}\n\n")
thisPar = [] thisPar = []
elif tType == self.T_TITLE: elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead) tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_UNNUM: elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead) tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD1: elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead) tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2: elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead) tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3: elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("### %s\n\n" % tHead) tmpResult.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4: elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n") tHead = tText.replace(r"\\", "\n")
tmpResult.append("#### %s\n\n" % tHead) tmpResult.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP: elif tType == self.T_SEP:
tmpResult.append("%s\n\n" % tText) tmpResult.append("%s\n\n" % tText)
@@ -141,10 +141,12 @@ class ToMarkdown(Tokenizer):
thisPar.append(tTemp.rstrip()) thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis: 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: 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: elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle)) tmpResult.append(self._formatKeywords(tText, tStyle))
@@ -189,7 +191,7 @@ class ToMarkdown(Tokenizer):
retText = "" retText = ""
if theBits[0] in nwLabels.KEY_NAME: 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: if len(theBits) > 1:
retText += ", ".join(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", "meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible: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 # Mimetype and Version
X_MIME = "application/vnd.oasis.opendocument.text" X_MIME = "application/vnd.oasis.opendocument.text"
X_VERS = "1.2" X_VERS = "1.2"
# Text Formatting Tags # Text Formatting Tags
TAG_BR = "{%s}line-break" % XML_NS["text"] TAG_BR = _mkTag("text", "line-break")
TAG_SPC = "{%s}s" % XML_NS["text"] TAG_SPC = _mkTag("text", "s")
TAG_NSPC = "{%s}c" % XML_NS["text"] TAG_NSPC = _mkTag("text", "c")
TAG_TAB = "{%s}tab" % XML_NS["text"] TAG_TAB = _mkTag("text", "tab")
TAG_SPAN = "{%s}span" % XML_NS["text"] TAG_SPAN = _mkTag("text", "span")
TAG_STNM = "{%s}style-name" % XML_NS["text"] TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes # Formatting Codes
X_BLD = 0x01 # Bold format X_BLD = 0x01 # Bold format
@@ -313,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data # Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date")) 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 = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}" xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -472,24 +489,22 @@ class ToOdt(Tokenizer):
def saveOpenDocText(self, savePath): def saveOpenDocText(self, savePath):
"""Save the data to an .odt file. """Save the data to an .odt file.
""" """
mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"} mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
mMani = "{%s}manifest" % mMap["manifest"] mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
mVers = "{%s}version" % mMap["manifest"] mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
mPath = "{%s}full-path" % mMap["manifest"] mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
mType = "{%s}media-type" % mMap["manifest"] mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
mFile = "{%s}file-entry" % mMap["manifest"]
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: "/", mVers: X_VERS, mType: X_MIME})
etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"}) 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: "content.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "meta.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"}) etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"} oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
oRoot = "{%s}document-settings" % sMap["office"] oSett = _mkTag("office", "settings", nsMap=OFFICE_NS)
oSett = "{%s}settings" % sMap["office"] xSett = etree.Element(oRoot, nsmap=OFFICE_NS)
xSett = etree.Element(oRoot, nsmap=sMap)
etree.SubElement(xSett, oSett) etree.SubElement(xSett, oSett)
with ZipFile(savePath, mode="w") as outFile: with ZipFile(savePath, mode="w") as outFile:
@@ -520,16 +535,16 @@ class ToOdt(Tokenizer):
"""Apply formatting to synopsis lines. """Apply formatting to synopsis lines.
""" """
sSynop = self._localLookup("Synopsis") sSynop = self._localLookup("Synopsis")
rTxt = "**%s:** %s" % (sSynop, tText) rTxt = "**{0}:** {1}".format(sSynop, tText)
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText)) rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(tText))
return rTxt, rFmt return rTxt, rFmt
def _formatComments(self, tText): def _formatComments(self, tText):
"""Apply formatting to comments. """Apply formatting to comments.
""" """
sComm = self._localLookup("Comment") sComm = self._localLookup("Comment")
rTxt = "**%s:** %s" % (sComm, tText) rTxt = "**{0}:** {1}".format(sComm, tText)
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText)) rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(tText))
return rTxt, rFmt return rTxt, rFmt
def _formatKeywords(self, tText): def _formatKeywords(self, tText):
@@ -543,8 +558,8 @@ class ToOdt(Tokenizer):
rFmt = "" rFmt = ""
if theBits[0] in nwLabels.KEY_NAME: if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]] tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText rTxt += "**{0}:** ".format(tText)
rFmt += "_B%s b_ " % (" "*len(tText)) rFmt += "_B{0} b_ ".format(" "*len(tText))
if len(theBits) > 1: if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += theBits[1] rTxt += theBits[1]
@@ -1468,16 +1483,4 @@ class XMLParagraph():
return return
# END Class XMLParagraph
# =============================================================================================== #
# 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
+5 -5
View File
@@ -158,7 +158,7 @@ class NWTree():
continue continue
tFile = tHandle+".nwd" tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)): 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), os.path.join("content", tFile),
tItem.itemClass.name, tItem.itemClass.name,
tItem.itemLayout.name, tItem.itemLayout.name,
@@ -175,7 +175,7 @@ class NWTree():
outFile.write("Table of Contents\n") outFile.write("Table of Contents\n")
outFile.write("=================\n") outFile.write("=================\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" "File Name", "Class", "Layout", "Document Label"
)) ))
outFile.write("-"*max(tocLen, 62) + "\n") outFile.write("-"*max(tocLen, 62) + "\n")
@@ -285,12 +285,12 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for i in range(nwConst.MAX_DEPTH + 1): for _ in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
tHandle = tItem.itemParent tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
return tTree return tTree
else: else:
@@ -341,7 +341,7 @@ class NWTree():
if tItem is None: if tItem is None:
return False return False
if tItem.itemType != nwItemType.FILE: 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 return False
if not isinstance(itemLayout, nwItemLayout): if not isinstance(itemLayout, nwItemLayout):
return False return False
+6 -6
View File
@@ -65,7 +65,7 @@ class GuiAbout(QDialog):
self.nwIcon = QLabel() self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("<b>novelWriter</b>") 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.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x"))
self.leftBox = QVBoxLayout() 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.tr("Translations"),
self._wrapTable([ self._wrapTable([
("English", "Veronica Berglyd Olsen"), ("English", "Veronica Berglyd Olsen"),
@@ -193,7 +193,7 @@ class GuiAbout(QDialog):
theIcons = self.theParent.theTheme.theIcons theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName and theTheme.themeAuthor != "N/A": if theTheme.themeName and theTheme.themeAuthor != "N/A":
licURL = f"<a href='{theTheme.themeLicenseUrl}'>{theTheme.themeLicense}</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.tr("Theme: {0}").format(theTheme.themeName),
self._wrapTable([ self._wrapTable([
(self.tr("Author"), theTheme.themeAuthor), (self.tr("Author"), theTheme.themeAuthor),
@@ -204,7 +204,7 @@ class GuiAbout(QDialog):
if theIcons.themeName: if theIcons.themeName:
licURL = f"<a href='{theIcons.themeLicenseUrl}'>{theIcons.themeLicense}</a>" 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.tr("Icons: {0}").format(theIcons.themeName),
self._wrapTable([ self._wrapTable([
(self.tr("Author"), theIcons.themeAuthor), (self.tr("Author"), theIcons.themeAuthor),
@@ -215,7 +215,7 @@ class GuiAbout(QDialog):
if theTheme.syntaxName: if theTheme.syntaxName:
licURL = f"<a href='{theTheme.syntaxLicenseUrl}'>{theTheme.syntaxLicense}</a>" 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.tr("Syntax: {0}").format(theTheme.syntaxName),
self._wrapTable([ self._wrapTable([
(self.tr("Author"), theTheme.syntaxAuthor), (self.tr("Author"), theTheme.syntaxAuthor),
@@ -258,7 +258,7 @@ class GuiAbout(QDialog):
theTable.append( theTable.append(
f"<tr><td><b>{aLabel}:</b></td><td>{aValue}</td></tr>" 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): def _setStyleSheet(self):
"""Set stylesheet for all browser tabs """Set stylesheet for all browser tabs
+1 -1
View File
@@ -55,7 +55,7 @@ class GuiDocMerge(QDialog):
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Merge Documents")) 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.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText 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.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document")) 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.helpLabel = QHelpLabel(
self.tr("Select the maximum level to split into files."), self.tr("Select the maximum level to split into files."),
self.theParent.theTheme.helpText self.theParent.theTheme.helpText
@@ -174,7 +174,7 @@ class GuiDocSplit(QDialog):
msgYes = self.theParent.askQuestion( msgYes = self.theParent.askQuestion(
self.tr("Split Document"), self.tr("Split Document"),
"%s<br><br>%s" % ( "{0}<br><br>{1}".format(
self.tr( self.tr(
"The document will be split into {0} file(s) in a new folder. " "The document will be split into {0} file(s) in a new folder. "
"The original document will remain intact.").format(nFiles), "The original document will remain intact.").format(nFiles),
@@ -196,10 +196,8 @@ class GuiDocSplit(QDialog):
# Loop through, and create the files # Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder: for wTitle, iStart, iEnd in finalOrder:
if srcItem.itemClass == nwItemClass.NOVEL: isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
else:
itemLayout = nwItemLayout.NOTE
wTitle = wTitle.lstrip("#") wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip() wTitle = wTitle.strip()
@@ -209,9 +207,8 @@ class GuiDocSplit(QDialog):
newItem.setLayout(itemLayout) newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
logger.verbose( logger.verbose(
"Creating new document %s with text from line %d to %d" % ( "Creating new document '%s' with text from line %d to %d",
nHandle, iStart+1, iEnd nHandle, iStart+1, iEnd
)
) )
theText = "\n".join(self.sourceText[iStart:iEnd]) theText = "\n".join(self.sourceText[iStart:iEnd])
@@ -273,7 +270,8 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData() spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel) self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug( 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() self.sourceText = theText.splitlines()
+2 -1
View File
@@ -57,7 +57,8 @@ class GuiItemEditor(QDialog):
self.theItem = self.theProject.projTree[tHandle] self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None: if self.theItem is None:
self._doClose() self.close()
return
self.setWindowTitle(self.tr("Item Settings")) self.setWindowTitle(self.tr("Item Settings"))
+1 -1
View File
@@ -719,7 +719,7 @@ class GuiDocEditor(QTextEdit):
), nwAlert.INFO) ), nwAlert.INFO)
theMode = False theMode = False
if self.spEnchant.spellLanguage is None: if self.spEnchant.spellLanguage() is None:
theMode = False theMode = False
self._spellCheck = theMode self._spellCheck = theMode
+18 -20
View File
@@ -784,7 +784,7 @@ class GuiMain(QMainWindow):
def passDocumentAction(self, theAction): def passDocumentAction(self, theAction):
"""Pass on document action to the document viewer if it has """Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of 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. action.
""" """
if self.docViewer.hasFocus(): if self.docViewer.hasFocus():
@@ -836,13 +836,13 @@ class GuiMain(QMainWindow):
if tHandle is None: if tHandle is None:
logger.warning("No item selected") logger.warning("No item selected")
return return False
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.projTree[tHandle]
if tItem is None: if tItem is None:
return return False
if tItem.itemType not in nwLists.REG_TYPES: if tItem.itemType not in nwLists.REG_TYPES:
return return False
logger.verbose("Requesting change to item '%s'", tHandle) logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle) dlgProj = GuiItemEditor(self, tHandle)
@@ -853,7 +853,7 @@ class GuiMain(QMainWindow):
self.docEditor.updateDocInfo(tHandle) self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle) self.docViewer.updateDocInfo(tHandle)
return return True
def rebuildTrees(self): def rebuildTrees(self):
"""Rebuild the project tree. """Rebuild the project tree.
@@ -938,7 +938,7 @@ class GuiMain(QMainWindow):
## ##
def showProjectLoadDialog(self): 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 projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached. Selecting to create a browse button for projects not yet cached. Selecting to create a
new project is forwarded to the new project wizard. new project is forwarded to the new project wizard.
@@ -1058,7 +1058,7 @@ class GuiMain(QMainWindow):
return return
def showWritingStatsDialog(self): def showWritingStatsDialog(self):
"""Open the session log dialog. """Open the session stats dialog.
""" """
if not self.hasProject: if not self.hasProject:
logger.error("No project open") logger.error("No project open")
@@ -1092,17 +1092,17 @@ class GuiMain(QMainWindow):
if showNotes: if showNotes:
dlgAbout.showReleaseNotes() dlgAbout.showReleaseNotes()
return return True
def showAboutQtDialog(self): def showAboutQtDialog(self):
"""Show the about dialog for Qt. """Show the about dialog for Qt.
""" """
msgBox = QMessageBox() msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt") msgBox.aboutQt(self, "About Qt")
return return True
def showUpdatesDialog(self): def showUpdatesDialog(self):
"""Show the updates dialog for novelWriter. """Show the check for updates dialog.
""" """
dlgUpdate = getGuiItem("GuiUpdates") dlgUpdate = getGuiItem("GuiUpdates")
if dlgUpdate is None: if dlgUpdate is None:
@@ -1117,11 +1117,11 @@ class GuiMain(QMainWindow):
return return
def makeAlert(self, theMessage, theLevel=nwAlert.INFO): def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message """Alert both the user and the logger at the same time. The
can be either a string or an array of strings. message can be either a string or a list of strings.
""" """
if isinstance(theMessage, list): if isinstance(theMessage, list):
theMessage = list(filter(None, theMessage)) theMessage = list(filter(None, theMessage)) # Strip empty strings
popMsg = "<br>".join(theMessage) popMsg = "<br>".join(theMessage)
logMsg = theMessage logMsg = theMessage
else: else:
@@ -1246,13 +1246,11 @@ class GuiMain(QMainWindow):
self.theProject.setLastViewed(None) self.theProject.setLastViewed(None)
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
self.splitView.setVisible(False) self.splitView.setVisible(False)
vPos = [bPos[1], 0] self.splitDocs.setSizes([bPos[1], 0])
self.splitDocs.setSizes(vPos)
return not self.splitView.isVisible() return not self.splitView.isVisible()
def toggleFocusMode(self): def toggleFocusMode(self):
"""Main GUI Focus Mode hides tree, view pane and optionally also """Main GUI Focus Mode hides tree, view, statusbar and menu.
statusbar and menu.
""" """
if self.docEditor.docHandle() is None: if self.docEditor.docHandle() is None:
logger.error("No document open, so not activating Focus Mode") logger.error("No document open, so not activating Focus Mode")
@@ -1409,7 +1407,7 @@ class GuiMain(QMainWindow):
return True return True
def _autoSaveProject(self): 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.hasProject
doSave &= self.theProject.projChanged doSave &= self.theProject.projChanged
@@ -1422,7 +1420,7 @@ class GuiMain(QMainWindow):
return return
def _autoSaveDocument(self): def _autoSaveDocument(self):
"""Triggered by the auto-save document timer to save the """Triggered by the autosave document timer to save the
document. document.
""" """
if self.hasProject and self.docEditor.docChanged(): if self.hasProject and self.docEditor.docChanged():
@@ -1511,7 +1509,7 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def _timeTick(self): def _timeTick(self):
"""Triggered on every tick of the timer. """Triggered on every tick of the main timer.
""" """
if not self.hasProject: if not self.hasProject:
return 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.guimain import GuiMain
from novelwriter.common import ( from novelwriter.common import (
checkString, checkInt, checkBool, checkHandle, isHandle, isTitleTag, checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
isItemClass, isItemType, isItemLayout, hexToInt, formatInt, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, formatInt,
formatTimeStamp, formatTime, parseTimeStamp, splitVersionNumber, formatTimeStamp, formatTime, parseTimeStamp, splitVersionNumber,
transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile, transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile,
makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
@@ -68,6 +68,20 @@ def testBaseCommon_CheckInt():
# END Test 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 @pytest.mark.base
def testBaseCommon_CheckBool(): def testBaseCommon_CheckBool():
"""Test the checkBool function. """Test the checkBool function.
+3 -3
View File
@@ -73,7 +73,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
assert theOpts.loadSettings() assert theOpts.loadSettings()
# Check that unwanted items have been removed # Check that unwanted items have been removed
assert theOpts.theState == { assert theOpts._theState == {
"GuiBuildNovel": { "GuiBuildNovel": {
"winWidth": 1000, "winWidth": 1000,
"winHeight": 700, "winHeight": 700,
@@ -88,7 +88,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
# Load again to check we get the values back # Load again to check we get the values back
assert theOpts.loadSettings() assert theOpts.loadSettings()
assert theOpts.theState == { assert theOpts._theState == {
"GuiBuildNovel": { "GuiBuildNovel": {
"winWidth": 1000, "winWidth": 1000,
"winHeight": 700, "winHeight": 700,
@@ -129,7 +129,7 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.getValue("GuiBuildNovel", "mockItem", None) is None assert theOpts.getValue("GuiBuildNovel", "mockItem", None) is None
# Get type-specific # 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.getString("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100 assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None 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(None) is False
assert spChk._readProjectDictionary(wList) is True assert spChk._readProjectDictionary(wList) is True
assert spChk.projectDict == wList assert spChk._projectDict == wList
# Cannot write to file # Cannot write to file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
+55 -55
View File
@@ -27,59 +27,6 @@ from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToHtml 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 @pytest.mark.core
def testCoreToHtml_ConvertFormat(mockGUI): def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class. """Test the tokenizer and converter chain using the ToHtml class.
@@ -433,7 +380,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToHtml_Complex(mockGUI, fncDir): def testCoreToHtml_Complex(mockGUI, fncDir):
"""Test the ave method of the ToHtml class. """Test the save method of the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
@@ -524,7 +471,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
theHtml.saveHTML5(saveFile) theHtml.saveHTML5(saveFile)
assert readFile(saveFile) == htmlDoc assert readFile(saveFile) == htmlDoc
# END Test testCoreToHtml_Save # END Test testCoreToHtml_Complex
@pytest.mark.core @pytest.mark.core
@@ -589,3 +536,56 @@ def testCoreToHtml_Methods(mockGUI):
assert theHtml.getStyleSheet() == [] assert theHtml.getStyleSheet() == []
# END Test testCoreToHtml_Methods # 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 # 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 @pytest.mark.core
def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions. """Test the document save functions.
@@ -676,6 +732,36 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
# END Test testCoreToOdt_SaveFull # 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 @pytest.mark.core
def testCoreToOdt_ODTParagraphStyle(): def testCoreToOdt_ODTParagraphStyle():
"""Test the ODTParagraphStyle class. """Test the ODTParagraphStyle class.
+34 -18
View File
@@ -27,23 +27,22 @@ from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.dialogs import GuiAbout from novelwriter.dialogs import GuiAbout
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI): def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
"""Test the full about dialogs. """Test the novelWriter about dialogs.
""" """
# NW About # Block message box
monkeypatch.setattr(GuiAbout, "exec_", lambda *a: None) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
# 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") msgAbout = getGuiItem("GuiAbout")
assert isinstance(msgAbout, GuiAbout) assert isinstance(msgAbout, GuiAbout)
msgAbout.show()
assert msgAbout.pageAbout.document().characterCount() > 100 assert msgAbout.pageAbout.document().characterCount() > 100
assert msgAbout.pageNotes.document().characterCount() > 100 assert msgAbout.pageNotes.document().characterCount() > 100
@@ -59,12 +58,29 @@ def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI):
msgAbout.showReleaseNotes() msgAbout.showReleaseNotes()
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes 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() 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.enum import nwItemType, nwWidget
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -96,7 +92,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwMerge = getGuiItem("GuiDocMerge") nwMerge = getGuiItem("GuiDocMerge")
assert isinstance(nwMerge, GuiDocMerge) assert isinstance(nwMerge, GuiDocMerge)
nwMerge.show() nwMerge.show()
qtbot.wait(stepDelay) qtbot.wait(50)
# Populate List # Populate List
# ============= # =============
@@ -32,10 +32,6 @@ from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -103,7 +99,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit = getGuiItem("GuiDocSplit") nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit) assert isinstance(nwSplit, GuiDocSplit)
nwSplit.show() nwSplit.show()
qtbot.wait(stepDelay) qtbot.wait(50)
# Populate List # Populate List
# ============= # =============
@@ -253,7 +249,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit.sourceItem = None nwSplit.sourceItem = None
assert nwSplit._doSplit() is False assert nwSplit._doSplit() is False
# Close up # Close
nwSplit._doClose() nwSplit._doClose()
# qtbot.stopForInteraction() # 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 pytest
import os
from shutil import copyfile from tools import getGuiItem
from tools import cmpFiles, getGuiItem
from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtWidgets import QAction, QDialog, QMessageBox
from novelwriter.gui import GuiProjectTree from novelwriter.gui import GuiProjectTree
from novelwriter.enum import nwItemLayout from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.dialogs import GuiItemEditor from novelwriter.dialogs import GuiItemEditor
from novelwriter.core.tree import NWTree
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the full item editor dialog. """Test launching the item editor dialog from GuiMain.
""" """
projFile = os.path.join(fncProj, "nwProject.nwx") # Block message box
testFile = os.path.join(outDir, "guiItemEditor_Dialog_nwProject.nwx") monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
compFile = os.path.join(refDir, "guiItemEditor_Dialog_nwProject.nwx")
# 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 # Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create new, save, open project # Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42) nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.openDocument("0e17daca5f3e1") assert nwGUI.openDocument("0e17daca5f3e1")
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None) # Check that an invalid handle is managed
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger) itemEdit = GuiItemEditor(nwGUI, "whatever")
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) itemEdit.show()
itemEdit._doClose()
itemEdit = getGuiItem("GuiItemEditor") # Edit a Document
assert isinstance(itemEdit, GuiItemEditor) itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1")
itemEdit.show() itemEdit.show()
qtbot.addWidget(itemEdit) # Check Existing Settings
assert itemEdit.editName.text() == "New Scene" assert itemEdit.editName.text() == "New Scene"
assert itemEdit.editStatus.currentData() == "New" assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
assert itemEdit.editExport.isChecked() is True
for c in "Just a Page": # Change Settings
qtbot.keyClick(itemEdit.editName, c, delay=typeDelay) layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE)
itemEdit.editName.setText("Great Scene")
itemEdit.editStatus.setCurrentIndex(1) itemEdit.editStatus.setCurrentIndex(1)
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.DOCUMENT)
itemEdit.editLayout.setCurrentIndex(layoutIdx) itemEdit.editLayout.setCurrentIndex(layoutIdx)
itemEdit.editExport.setChecked(False) itemEdit.editExport.setChecked(False)
assert not itemEdit.editExport.isChecked()
# Check New Settings
itemEdit._doSave() 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) # Check that the editor header is updated
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
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1") nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Just a Page" assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Great Scene"
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])
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
# END Test testDlgItemEditor_Dialog # 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