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