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