Change editor to true plain text (#1525)
This commit is contained in:
@@ -73,7 +73,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# Main Program
|
# Main Program
|
||||||
##
|
##
|
||||||
|
|
||||||
# Global config singleton
|
# Global config and data singletons
|
||||||
CONFIG = Config()
|
CONFIG = Config()
|
||||||
SHARED = SharedData()
|
SHARED = SharedData()
|
||||||
|
|
||||||
|
|||||||
+30
-34
@@ -111,7 +111,7 @@ class ToHtml(Tokenizer):
|
|||||||
|
|
||||||
def getFullResultSize(self) -> int:
|
def getFullResultSize(self) -> int:
|
||||||
"""Return the size of the full HTML result."""
|
"""Return the size of the full HTML result."""
|
||||||
return sum([len(x) for x in self._fullHTML])
|
return sum(len(x) for x in self._fullHTML)
|
||||||
|
|
||||||
def doPreProcessing(self) -> None:
|
def doPreProcessing(self) -> None:
|
||||||
"""Extend the auto-replace to also properly encode some unicode
|
"""Extend the auto-replace to also properly encode some unicode
|
||||||
@@ -122,9 +122,7 @@ class ToHtml(Tokenizer):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def doConvert(self) -> None:
|
def doConvert(self) -> None:
|
||||||
"""Convert the list of text tokens into a HTML document saved
|
"""Convert the list of text tokens into an HTML document."""
|
||||||
to _result.
|
|
||||||
"""
|
|
||||||
if self._genMode == self.M_PREVIEW:
|
if self._genMode == self.M_PREVIEW:
|
||||||
htmlTags = { # HTML4 + CSS2 (for Qt)
|
htmlTags = { # HTML4 + CSS2 (for Qt)
|
||||||
self.FMT_B_B: "<b>",
|
self.FMT_B_B: "<b>",
|
||||||
@@ -160,9 +158,9 @@ class ToHtml(Tokenizer):
|
|||||||
|
|
||||||
self._result = ""
|
self._result = ""
|
||||||
|
|
||||||
thisPar = []
|
para = []
|
||||||
parStyle = None
|
pStyle = None
|
||||||
tmpResult = []
|
lines = []
|
||||||
|
|
||||||
for tType, tLine, tText, tFormat, tStyle in self._tokens:
|
for tType, tLine, tText, tFormat, tStyle in self._tokens:
|
||||||
|
|
||||||
@@ -231,69 +229,67 @@ class ToHtml(Tokenizer):
|
|||||||
|
|
||||||
# Process Text Type
|
# Process Text Type
|
||||||
if tType == self.T_EMPTY:
|
if tType == self.T_EMPTY:
|
||||||
if parStyle is None:
|
if pStyle is None:
|
||||||
parStyle = ""
|
pStyle = ""
|
||||||
if len(thisPar) > 1 and self._cssStyles:
|
if len(para) > 1 and self._cssStyles:
|
||||||
parClass = " class='break'"
|
pClass = " class='break'"
|
||||||
else:
|
else:
|
||||||
parClass = ""
|
pClass = ""
|
||||||
if len(thisPar) > 0:
|
if len(para) > 0:
|
||||||
tTemp = "<br/>".join(thisPar)
|
tTemp = "<br/>".join(para)
|
||||||
tmpResult.append(f"<p{parClass+parStyle}>{tTemp.rstrip()}</p>\n")
|
lines.append(f"<p{pClass+pStyle}>{tTemp.rstrip()}</p>\n")
|
||||||
thisPar = []
|
para = []
|
||||||
parStyle = None
|
pStyle = None
|
||||||
|
|
||||||
elif tType == self.T_TITLE:
|
elif tType == self.T_TITLE:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
|
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
|
||||||
|
|
||||||
elif tType == self.T_UNNUM:
|
elif tType == self.T_UNNUM:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD1:
|
elif tType == self.T_HEAD1:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
|
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD2:
|
elif tType == self.T_HEAD2:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD3:
|
elif tType == self.T_HEAD3:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
|
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD4:
|
elif tType == self.T_HEAD4:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||||
tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
|
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
|
||||||
|
|
||||||
elif tType == self.T_SEP:
|
elif tType == self.T_SEP:
|
||||||
tmpResult.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
|
lines.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
|
||||||
|
|
||||||
elif tType == self.T_SKIP:
|
elif tType == self.T_SKIP:
|
||||||
tmpResult.append(f"<p class='skip'{hStyle}> </p>\n")
|
lines.append(f"<p class='skip'{hStyle}> </p>\n")
|
||||||
|
|
||||||
elif tType == self.T_TEXT:
|
elif tType == self.T_TEXT:
|
||||||
tTemp = tText
|
tTemp = tText
|
||||||
if parStyle is None:
|
if pStyle is None:
|
||||||
parStyle = hStyle
|
pStyle = hStyle
|
||||||
for xPos, xLen, xFmt in reversed(tFormat):
|
for xPos, xLen, xFmt in reversed(tFormat):
|
||||||
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
||||||
thisPar.append(stripEscape(tTemp.rstrip()))
|
para.append(stripEscape(tTemp.rstrip()))
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
tmpResult.append(self._formatSynopsis(tText))
|
lines.append(self._formatSynopsis(tText))
|
||||||
|
|
||||||
elif tType == self.T_COMMENT and self._doComments:
|
elif tType == self.T_COMMENT and self._doComments:
|
||||||
tmpResult.append(self._formatComments(tText))
|
lines.append(self._formatComments(tText))
|
||||||
|
|
||||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||||
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
|
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
|
||||||
tmpResult.append(tTemp)
|
lines.append(tTemp)
|
||||||
|
|
||||||
self._result = "".join(tmpResult)
|
|
||||||
tmpResult = []
|
|
||||||
|
|
||||||
|
self._result = "".join(lines)
|
||||||
if self._genMode != self.M_PREVIEW:
|
if self._genMode != self.M_PREVIEW:
|
||||||
self._fullHTML.append(self._result)
|
self._fullHTML.append(self._result)
|
||||||
|
|
||||||
|
|||||||
+25
-28
@@ -65,10 +65,12 @@ class ToMarkdown(Tokenizer):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def setStandardMarkdown(self) -> None:
|
def setStandardMarkdown(self) -> None:
|
||||||
|
"""Set the converter to use standard Markdown formatting."""
|
||||||
self._genMode = self.M_STD
|
self._genMode = self.M_STD
|
||||||
return
|
return
|
||||||
|
|
||||||
def setGitHubMarkdown(self) -> None:
|
def setGitHubMarkdown(self) -> None:
|
||||||
|
"""Set the converter to use GitHub Markdown formatting."""
|
||||||
self._genMode = self.M_GH
|
self._genMode = self.M_GH
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -78,12 +80,10 @@ class ToMarkdown(Tokenizer):
|
|||||||
|
|
||||||
def getFullResultSize(self) -> int:
|
def getFullResultSize(self) -> int:
|
||||||
"""Return the size of the full Markdown result."""
|
"""Return the size of the full Markdown result."""
|
||||||
return sum([len(x) for x in self._fullMD])
|
return sum(len(x) for x in self._fullMD)
|
||||||
|
|
||||||
def doConvert(self) -> None:
|
def doConvert(self) -> None:
|
||||||
"""Convert the list of text tokens into a HTML document saved
|
"""Convert the list of text tokens into a Markdown document."""
|
||||||
to theResult.
|
|
||||||
"""
|
|
||||||
if self._genMode == self.M_STD:
|
if self._genMode == self.M_STD:
|
||||||
# Standard
|
# Standard
|
||||||
mdTags = {
|
mdTags = {
|
||||||
@@ -107,68 +107,65 @@ class ToMarkdown(Tokenizer):
|
|||||||
|
|
||||||
self._result = ""
|
self._result = ""
|
||||||
|
|
||||||
thisPar = []
|
para = []
|
||||||
tmpResult = []
|
lines = []
|
||||||
|
|
||||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||||
|
|
||||||
# Process Text Type
|
|
||||||
if tType == self.T_EMPTY:
|
if tType == self.T_EMPTY:
|
||||||
if len(thisPar) > 0:
|
if len(para) > 0:
|
||||||
tTemp = (" \n".join(thisPar)).rstrip(" ")
|
tTemp = (" \n".join(para)).rstrip(" ")
|
||||||
tmpResult.append(f"{tTemp}\n\n")
|
lines.append(f"{tTemp}\n\n")
|
||||||
thisPar = []
|
para = []
|
||||||
|
|
||||||
elif tType == self.T_TITLE:
|
elif tType == self.T_TITLE:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"# {tHead}\n\n")
|
lines.append(f"# {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_UNNUM:
|
elif tType == self.T_UNNUM:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"## {tHead}\n\n")
|
lines.append(f"## {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD1:
|
elif tType == self.T_HEAD1:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"# {tHead}\n\n")
|
lines.append(f"# {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD2:
|
elif tType == self.T_HEAD2:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"## {tHead}\n\n")
|
lines.append(f"## {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD3:
|
elif tType == self.T_HEAD3:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"### {tHead}\n\n")
|
lines.append(f"### {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_HEAD4:
|
elif tType == self.T_HEAD4:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
tmpResult.append(f"#### {tHead}\n\n")
|
lines.append(f"#### {tHead}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_SEP:
|
elif tType == self.T_SEP:
|
||||||
tmpResult.append("%s\n\n" % tText)
|
lines.append(f"{tText}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_SKIP:
|
elif tType == self.T_SKIP:
|
||||||
tmpResult.append("\n\n\n")
|
lines.append("\n\n\n")
|
||||||
|
|
||||||
elif tType == self.T_TEXT:
|
elif tType == self.T_TEXT:
|
||||||
tTemp = tText
|
tTemp = tText
|
||||||
for xPos, xLen, xFmt in reversed(tFormat):
|
for xPos, xLen, xFmt in reversed(tFormat):
|
||||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
||||||
thisPar.append(tTemp.rstrip())
|
para.append(tTemp.rstrip())
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
locName = self._localLookup("Synopsis")
|
label = self._localLookup("Synopsis")
|
||||||
tmpResult.append(f"**{locName}:** {tText}\n\n")
|
lines.append(f"**{label}:** {tText}\n\n")
|
||||||
|
|
||||||
elif tType == self.T_COMMENT and self._doComments:
|
elif tType == self.T_COMMENT and self._doComments:
|
||||||
locName = self._localLookup("Comment")
|
label = self._localLookup("Comment")
|
||||||
tmpResult.append(f"**{locName}:** {tText}\n\n")
|
lines.append(f"**{label}:** {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))
|
lines.append(self._formatKeywords(tText, tStyle))
|
||||||
|
|
||||||
self._result = "".join(tmpResult)
|
|
||||||
tmpResult = []
|
|
||||||
|
|
||||||
|
self._result = "".join(lines)
|
||||||
self._fullMD.append(self._result)
|
self._fullMD.append(self._result)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
+60
-60
@@ -27,10 +27,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from pathlib import Path
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -394,9 +394,9 @@ class ToOdt(Tokenizer):
|
|||||||
self.FMT_D_E: "s_", # Strikethrough close format
|
self.FMT_D_E: "s_", # Strikethrough close format
|
||||||
}
|
}
|
||||||
|
|
||||||
thisPar = []
|
fmt = []
|
||||||
thisFmt = []
|
para = []
|
||||||
parStyle = None
|
pStyle = None
|
||||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||||
|
|
||||||
# Styles
|
# Styles
|
||||||
@@ -429,20 +429,20 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
# Process Text Types
|
# Process Text Types
|
||||||
if tType == self.T_EMPTY:
|
if tType == self.T_EMPTY:
|
||||||
if len(thisPar) > 1 and parStyle is not None:
|
if len(para) > 1 and pStyle is not None:
|
||||||
if self._doJustify:
|
if self._doJustify:
|
||||||
parStyle.setTextAlign("left")
|
pStyle.setTextAlign("left")
|
||||||
|
|
||||||
if len(thisPar) > 0 and parStyle is not None:
|
if len(para) > 0 and pStyle is not None:
|
||||||
tTemp = "\n".join(thisPar)
|
tTemp = "\n".join(para)
|
||||||
fTemp = " ".join(thisFmt)
|
fTemp = " ".join(fmt)
|
||||||
tTxt = tTemp.rstrip()
|
tTxt = tTemp.rstrip()
|
||||||
tFmt = fTemp[:len(tTxt)]
|
tFmt = fTemp[:len(tTxt)]
|
||||||
self._addTextPar("Text_20_body", parStyle, tTxt, tFmt=tFmt)
|
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
|
||||||
|
|
||||||
thisPar = []
|
fmt = []
|
||||||
thisFmt = []
|
para = []
|
||||||
parStyle = None
|
pStyle = None
|
||||||
|
|
||||||
elif tType == self.T_TITLE:
|
elif tType == self.T_TITLE:
|
||||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||||
@@ -475,8 +475,8 @@ class ToOdt(Tokenizer):
|
|||||||
self._addTextPar("Separator", oStyle, "")
|
self._addTextPar("Separator", oStyle, "")
|
||||||
|
|
||||||
elif tType == self.T_TEXT:
|
elif tType == self.T_TEXT:
|
||||||
if parStyle is None:
|
if pStyle is None:
|
||||||
parStyle = oStyle
|
pStyle = oStyle
|
||||||
|
|
||||||
tFmt = " "*len(tText)
|
tFmt = " "*len(tText)
|
||||||
for xPos, xLen, xFmt in tFormat:
|
for xPos, xLen, xFmt in tFormat:
|
||||||
@@ -484,8 +484,8 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
tTxt = tText.rstrip()
|
tTxt = tText.rstrip()
|
||||||
tFmt = tFmt[:len(tTxt)]
|
tFmt = tFmt[:len(tTxt)]
|
||||||
thisPar.append(tTxt)
|
para.append(tTxt)
|
||||||
thisFmt.append(tFmt)
|
fmt.append(tFmt)
|
||||||
|
|
||||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||||
tTemp, fTemp = self._formatSynopsis(tText)
|
tTemp, fTemp = self._formatSynopsis(tText)
|
||||||
@@ -501,8 +501,8 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def closeDocument(self):
|
def closeDocument(self) -> None:
|
||||||
"""Return the serialised XML document"""
|
"""Pack the styles of the XML document."""
|
||||||
# Build the auto-generated styles
|
# Build the auto-generated styles
|
||||||
for styleName, styleObj in self._autoPara.values():
|
for styleName, styleObj in self._autoPara.values():
|
||||||
styleObj.packXML(self._xAuto, styleName)
|
styleObj.packXML(self._xAuto, styleName)
|
||||||
@@ -510,7 +510,7 @@ class ToOdt(Tokenizer):
|
|||||||
styleObj.packXML(self._xAuto, styleName)
|
styleObj.packXML(self._xAuto, styleName)
|
||||||
return
|
return
|
||||||
|
|
||||||
def saveFlatXML(self, path: str | Path):
|
def saveFlatXML(self, path: str | Path) -> None:
|
||||||
"""Save the data to an .fodt file."""
|
"""Save the data to an .fodt file."""
|
||||||
with open(path, mode="wb") as fObj:
|
with open(path, mode="wb") as fObj:
|
||||||
xml = ET.ElementTree(self._dFlat)
|
xml = ET.ElementTree(self._dFlat)
|
||||||
@@ -519,7 +519,7 @@ class ToOdt(Tokenizer):
|
|||||||
logger.info("Wrote file: %s", path)
|
logger.info("Wrote file: %s", path)
|
||||||
return
|
return
|
||||||
|
|
||||||
def saveOpenDocText(self, path: str | Path):
|
def saveOpenDocText(self, path: str | Path) -> None:
|
||||||
"""Save the data to an .odt file."""
|
"""Save the data to an .odt file."""
|
||||||
mMani = _mkTag("manifest", "manifest")
|
mMani = _mkTag("manifest", "manifest")
|
||||||
mVers = _mkTag("manifest", "version")
|
mVers = _mkTag("manifest", "version")
|
||||||
@@ -718,9 +718,9 @@ class ToOdt(Tokenizer):
|
|||||||
|
|
||||||
return newName
|
return newName
|
||||||
|
|
||||||
def _emToCm(self, emVal: float) -> str:
|
def _emToCm(self, value: float) -> str:
|
||||||
"""Converts an em value to centimetres."""
|
"""Converts an em value to centimetres."""
|
||||||
return f"{emVal*2.54/72*self._textSize:.3f}cm"
|
return f"{value*2.54/72*self._textSize:.3f}cm"
|
||||||
|
|
||||||
##
|
##
|
||||||
# Style Elements
|
# Style Elements
|
||||||
@@ -1193,56 +1193,56 @@ class ODTParagraphStyle:
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def checkNew(self, refStyle: ODTParagraphStyle) -> bool:
|
def checkNew(self, style: ODTParagraphStyle) -> bool:
|
||||||
"""Check if there are new settings in refStyle that differ from
|
"""Check if there are new settings in refStyle that differ from
|
||||||
those in the current object.
|
those in the current object.
|
||||||
"""
|
"""
|
||||||
for aName, (_, aVal) in refStyle._mAttr.items():
|
for name, (_, aVal) in style._mAttr.items():
|
||||||
if aVal is not None and aVal != self._mAttr[aName][1]:
|
if aVal is not None and aVal != self._mAttr[name][1]:
|
||||||
return True
|
return True
|
||||||
for aName, (_, aVal) in refStyle._pAttr.items():
|
for name, (_, aVal) in style._pAttr.items():
|
||||||
if aVal is not None and aVal != self._pAttr[aName][1]:
|
if aVal is not None and aVal != self._pAttr[name][1]:
|
||||||
return True
|
return True
|
||||||
for aName, (_, aVal) in refStyle._tAttr.items():
|
for name, (_, aVal) in style._tAttr.items():
|
||||||
if aVal is not None and aVal != self._tAttr[aName][1]:
|
if aVal is not None and aVal != self._tAttr[name][1]:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def getID(self) -> str:
|
def getID(self) -> str:
|
||||||
"""Generate a unique ID from the settings."""
|
"""Generate a unique ID from the settings."""
|
||||||
theString = (
|
string = (
|
||||||
f"Paragraph:Main:{str(self._mAttr)}:"
|
f"Paragraph:Main:{str(self._mAttr)}:"
|
||||||
f"Paragraph:Para:{str(self._pAttr)}:"
|
f"Paragraph:Para:{str(self._pAttr)}:"
|
||||||
f"Paragraph:Text:{str(self._tAttr)}:"
|
f"Paragraph:Text:{str(self._tAttr)}:"
|
||||||
)
|
)
|
||||||
return sha256(theString.encode()).hexdigest()
|
return sha256(string.encode()).hexdigest()
|
||||||
|
|
||||||
def packXML(self, xParent: ET.Element, name: str) -> None:
|
def packXML(self, xParent: ET.Element, name: str) -> None:
|
||||||
"""Pack the content into an xml element."""
|
"""Pack the content into an xml element."""
|
||||||
theAttr = {}
|
attr = {}
|
||||||
theAttr[_mkTag("style", "name")] = name
|
attr[_mkTag("style", "name")] = name
|
||||||
theAttr[_mkTag("style", "family")] = "paragraph"
|
attr[_mkTag("style", "family")] = "paragraph"
|
||||||
for aName, (aNm, aVal) in self._mAttr.items():
|
for aName, (aNm, aVal) in self._mAttr.items():
|
||||||
if aVal is not None:
|
if aVal is not None:
|
||||||
theAttr[_mkTag(aNm, aName)] = aVal
|
attr[_mkTag(aNm, aName)] = aVal
|
||||||
|
|
||||||
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
|
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
|
||||||
|
|
||||||
theAttr = {}
|
attr = {}
|
||||||
for aName, (aNm, aVal) in self._pAttr.items():
|
for aName, (aNm, aVal) in self._pAttr.items():
|
||||||
if aVal is not None:
|
if aVal is not None:
|
||||||
theAttr[_mkTag(aNm, aName)] = aVal
|
attr[_mkTag(aNm, aName)] = aVal
|
||||||
|
|
||||||
if theAttr:
|
if attr:
|
||||||
ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
|
ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=attr)
|
||||||
|
|
||||||
theAttr = {}
|
attr = {}
|
||||||
for aName, (aNm, aVal) in self._tAttr.items():
|
for aName, (aNm, aVal) in self._tAttr.items():
|
||||||
if aVal is not None:
|
if aVal is not None:
|
||||||
theAttr[_mkTag(aNm, aName)] = aVal
|
attr[_mkTag(aNm, aName)] = aVal
|
||||||
|
|
||||||
if theAttr:
|
if attr:
|
||||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1307,18 +1307,18 @@ class ODTTextStyle:
|
|||||||
|
|
||||||
def packXML(self, xParent: ET.Element, name: str) -> None:
|
def packXML(self, xParent: ET.Element, name: str) -> None:
|
||||||
"""Pack the content into an xml element."""
|
"""Pack the content into an xml element."""
|
||||||
theAttr = {}
|
attr = {}
|
||||||
theAttr[_mkTag("style", "name")] = name
|
attr[_mkTag("style", "name")] = name
|
||||||
theAttr[_mkTag("style", "family")] = "text"
|
attr[_mkTag("style", "family")] = "text"
|
||||||
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
|
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
|
||||||
|
|
||||||
theAttr = {}
|
attr = {}
|
||||||
for aName, (aNm, aVal) in self._tAttr.items():
|
for aName, (aNm, aVal) in self._tAttr.items():
|
||||||
if aVal is not None:
|
if aVal is not None:
|
||||||
theAttr[_mkTag(aNm, aName)] = aVal
|
attr[_mkTag(aNm, aName)] = aVal
|
||||||
|
|
||||||
if theAttr:
|
if attr:
|
||||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1368,17 +1368,17 @@ class XMLParagraph:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def appendText(self, tText: str) -> None:
|
def appendText(self, text: str) -> None:
|
||||||
"""Append text to the XML element. We do this one character at
|
"""Append text to the XML element. We do this one character at
|
||||||
the time in order to be able to process line breaks, tabs and
|
the time in order to be able to process line breaks, tabs and
|
||||||
spaces separately. Multiple spaces are concatenated into a
|
spaces separately. Multiple spaces are concatenated into a
|
||||||
single tag, and must therefore be processed separately.
|
single tag, and must therefore be processed separately.
|
||||||
"""
|
"""
|
||||||
tText = stripEscape(tText)
|
text = stripEscape(text)
|
||||||
nSpaces = 0
|
nSpaces = 0
|
||||||
self._rawTxt += tText
|
self._rawTxt += text
|
||||||
|
|
||||||
for c in tText:
|
for c in text:
|
||||||
if c == " ":
|
if c == " ":
|
||||||
nSpaces += 1
|
nSpaces += 1
|
||||||
continue
|
continue
|
||||||
@@ -1433,17 +1433,17 @@ class XMLParagraph:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def appendSpan(self, tText: str, tFmt: str) -> None:
|
def appendSpan(self, text: str, fmt: str) -> None:
|
||||||
"""Append a text span to the XML element. The span is always
|
"""Append a text span to the XML element. The span is always
|
||||||
closed since we do not allow nested spans (like Libre Office).
|
closed since we do not allow nested spans (like Libre Office).
|
||||||
Therefore we return to the root element level when we're done
|
Therefore we return to the root element level when we're done
|
||||||
processing the text of the span.
|
processing the text of the span.
|
||||||
"""
|
"""
|
||||||
self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: tFmt})
|
self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
|
||||||
self._xTail.text = "" # Defaults to None
|
self._xTail.text = "" # Defaults to None
|
||||||
self._xTail.tail = "" # Defaults to None
|
self._xTail.tail = "" # Defaults to None
|
||||||
self._nState = X_SPAN_TEXT
|
self._nState = X_SPAN_TEXT
|
||||||
self.appendText(tText)
|
self.appendText(text)
|
||||||
self._nState = X_ROOT_TAIL
|
self._nState = X_ROOT_TAIL
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+408
-487
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,8 @@ from time import time
|
|||||||
|
|
||||||
from PyQt5.QtCore import Qt, QRegularExpression
|
from PyQt5.QtCore import Qt, QRegularExpression
|
||||||
from PyQt5.QtGui import (
|
from PyQt5.QtGui import (
|
||||||
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush, QTextDocument
|
QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData,
|
||||||
|
QTextCharFormat, QTextDocument
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
@@ -38,6 +39,9 @@ from novelwriter.constants import nwRegEx, nwUnicode
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
|
||||||
|
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||||
|
|
||||||
|
|
||||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||||
|
|
||||||
@@ -51,9 +55,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
logger.debug("Create: GuiDocHighlighter")
|
logger.debug("Create: GuiDocHighlighter")
|
||||||
|
|
||||||
|
self._tItem = None
|
||||||
self._tHandle = None
|
self._tHandle = None
|
||||||
self._spellCheck = False
|
self._spellCheck = False
|
||||||
self._spellRx = QRegularExpression()
|
|
||||||
|
|
||||||
self._hRules: list[tuple[str, dict]] = []
|
self._hRules: list[tuple[str, dict]] = []
|
||||||
self._hStyles: dict[str, QTextCharFormat] = {}
|
self._hStyles: dict[str, QTextCharFormat] = {}
|
||||||
@@ -79,11 +83,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@property
|
|
||||||
def spellCheck(self) -> bool:
|
|
||||||
"""Check if spell checking is enabled."""
|
|
||||||
return self._spellCheck
|
|
||||||
|
|
||||||
def initHighlighter(self) -> None:
|
def initHighlighter(self) -> None:
|
||||||
"""Initialise the syntax highlighter, setting all the colour
|
"""Initialise the syntax highlighter, setting all the colour
|
||||||
rules and building the RegExes.
|
rules and building the RegExes.
|
||||||
@@ -227,13 +226,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||||
self.rxRules.append((hReg, regRules))
|
self.rxRules.append((hReg, regRules))
|
||||||
|
|
||||||
# Build a QRegExp for the spell checker
|
|
||||||
# Include additional characters that the highlighter should
|
|
||||||
# consider to be word separators
|
|
||||||
uCode = nwUnicode.U_ENDASH + nwUnicode.U_EMDASH
|
|
||||||
self._spellRx = QRegularExpression(r"\b[^\s\-\+\/" + uCode + r"]+\b")
|
|
||||||
self._spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -248,6 +240,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
def setHandle(self, tHandle: str) -> None:
|
def setHandle(self, tHandle: str) -> None:
|
||||||
"""Set the handle of the currently highlighted document."""
|
"""Set the handle of the currently highlighted document."""
|
||||||
self._tHandle = tHandle
|
self._tHandle = tHandle
|
||||||
|
self._tItem = SHARED.project.tree[tHandle]
|
||||||
|
logger.debug(
|
||||||
|
"Syntax highlighter %s for item '%s'",
|
||||||
|
"enabled" if self._tItem else "disabled", tHandle
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -284,27 +281,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
if text.startswith("@"): # Keywords and commands
|
if text.startswith("@"): # Keywords and commands
|
||||||
self.setCurrentBlockState(self.BLOCK_META)
|
self.setCurrentBlockState(self.BLOCK_META)
|
||||||
pIndex = SHARED.project.index
|
if self._tItem:
|
||||||
tItem = SHARED.project.tree[self._tHandle]
|
pIndex = SHARED.project.index
|
||||||
if tItem is None:
|
isValid, theBits, thePos = pIndex.scanThis(text)
|
||||||
return
|
isGood = pIndex.checkThese(theBits, self._tItem)
|
||||||
|
if isValid:
|
||||||
isValid, theBits, thePos = pIndex.scanThis(text)
|
for n, theBit in enumerate(theBits):
|
||||||
isGood = pIndex.checkThese(theBits, tItem)
|
xPos = thePos[n]
|
||||||
if isValid:
|
xLen = len(theBit)
|
||||||
for n, theBit in enumerate(theBits):
|
if isGood[n]:
|
||||||
xPos = thePos[n]
|
if n == 0:
|
||||||
xLen = len(theBit)
|
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||||
if isGood[n]:
|
else:
|
||||||
if n == 0:
|
self.setFormat(xPos, xLen, self._hStyles["value"])
|
||||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
|
||||||
else:
|
else:
|
||||||
self.setFormat(xPos, xLen, self._hStyles["value"])
|
kwFmt = self.format(xPos)
|
||||||
else:
|
kwFmt.setUnderlineColor(self._colError)
|
||||||
kwFmt = self.format(xPos)
|
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||||
kwFmt.setUnderlineColor(self._colError)
|
self.setFormat(xPos, xLen, kwFmt)
|
||||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
|
||||||
self.setFormat(xPos, xLen, kwFmt)
|
|
||||||
|
|
||||||
# We never want to run the spell checker on keyword/values,
|
# We never want to run the spell checker on keyword/values,
|
||||||
# so we force a return here
|
# so we force a return here
|
||||||
@@ -382,17 +376,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
spFmt.merge(xFmt[xM])
|
spFmt.merge(xFmt[xM])
|
||||||
self.setFormat(x, 1, spFmt)
|
self.setFormat(x, 1, spFmt)
|
||||||
|
|
||||||
if not self._spellCheck:
|
data = self.currentBlockUserData()
|
||||||
return
|
if not isinstance(data, TextBlockData):
|
||||||
|
data = TextBlockData()
|
||||||
|
self.setCurrentBlockUserData(data)
|
||||||
|
|
||||||
rxSpell = self._spellRx.globalMatch(text.replace("_", " "), 0)
|
if self._spellCheck:
|
||||||
while rxSpell.hasNext():
|
for xPos, xLen in data.spellCheck(text):
|
||||||
rxMatch = rxSpell.next()
|
|
||||||
if not SHARED.spelling.checkWord(rxMatch.captured(0)):
|
|
||||||
if not rxMatch.captured(0).isalpha() or rxMatch.captured(0).isupper():
|
|
||||||
continue
|
|
||||||
xPos = rxMatch.capturedStart(0)
|
|
||||||
xLen = rxMatch.capturedLength(0)
|
|
||||||
for x in range(xPos, xPos+xLen):
|
for x in range(xPos, xPos+xLen):
|
||||||
spFmt = self.format(x)
|
spFmt = self.format(x)
|
||||||
spFmt.setUnderlineColor(self._colSpell)
|
spFmt.setUnderlineColor(self._colSpell)
|
||||||
@@ -437,3 +427,33 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
return charFormat
|
return charFormat
|
||||||
|
|
||||||
# END Class GuiDocHighlighter
|
# END Class GuiDocHighlighter
|
||||||
|
|
||||||
|
|
||||||
|
class TextBlockData(QTextBlockUserData):
|
||||||
|
|
||||||
|
__slots__ = ("_spellErrors")
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._spellErrors: list[tuple[int, int]] = []
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spellErrors(self) -> list[tuple[int, int]]:
|
||||||
|
"""Return spell error data from last check."""
|
||||||
|
return self._spellErrors
|
||||||
|
|
||||||
|
def spellCheck(self, text: str) -> list[tuple[int, int]]:
|
||||||
|
"""Run the spell checker and cache the result, and return the
|
||||||
|
list of spell check errors.
|
||||||
|
"""
|
||||||
|
self._spellErrors = []
|
||||||
|
rxSpell = SPELLRX.globalMatch(text.replace("_", " "), 0)
|
||||||
|
while rxSpell.hasNext():
|
||||||
|
rxMatch = rxSpell.next()
|
||||||
|
if not SHARED.spelling.checkWord(rxMatch.captured(0)):
|
||||||
|
if not rxMatch.captured(0).isnumeric() and not rxMatch.captured(0).isupper():
|
||||||
|
self._spellErrors.append((rxMatch.capturedStart(0), rxMatch.capturedLength(0)))
|
||||||
|
return self._spellErrors
|
||||||
|
|
||||||
|
# END Class TextBlockData
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – GUI Text Document
|
||||||
|
===============================
|
||||||
|
|
||||||
|
File History:
|
||||||
|
Created: 2023-09-07 [2.2b1]
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2023, 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/>.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from PyQt5.QtGui import QTextCursor, QTextDocument
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp
|
||||||
|
from novelwriter import SHARED
|
||||||
|
|
||||||
|
from novelwriter.gui.dochighlight import GuiDocHighlighter, TextBlockData
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GuiTextDocument(QTextDocument):
|
||||||
|
|
||||||
|
def __init__(self, parent: QObject) -> None:
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self._handle = None
|
||||||
|
self._syntax = GuiDocHighlighter(self)
|
||||||
|
self.setDocumentLayout(QPlainTextDocumentLayout(self))
|
||||||
|
|
||||||
|
logger.debug("Ready: GuiTextDocument")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def __del__(self): # pragma: no cover
|
||||||
|
logger.debug("Delete: GuiTextDocument")
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def syntaxHighlighter(self) -> GuiDocHighlighter:
|
||||||
|
"""Return the document's syntax highlighter object."""
|
||||||
|
return self._syntax
|
||||||
|
|
||||||
|
##
|
||||||
|
# Metods
|
||||||
|
##
|
||||||
|
|
||||||
|
def setTextContent(self, text: str, tHandle: str) -> None:
|
||||||
|
"""Set the text content of the document."""
|
||||||
|
self._syntax.setHandle(tHandle)
|
||||||
|
|
||||||
|
self.blockSignals(True)
|
||||||
|
self.setUndoRedoEnabled(False)
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
tStart = time()
|
||||||
|
|
||||||
|
self.setPlainText(text)
|
||||||
|
count = self.lineCount()
|
||||||
|
|
||||||
|
tMid = time()
|
||||||
|
|
||||||
|
self.setUndoRedoEnabled(True)
|
||||||
|
self.blockSignals(False)
|
||||||
|
self._syntax.rehighlight()
|
||||||
|
qApp.processEvents()
|
||||||
|
|
||||||
|
tEnd = time()
|
||||||
|
|
||||||
|
logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart))
|
||||||
|
logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]:
|
||||||
|
"""Check if there is a misspelled word at a given position in
|
||||||
|
the document, and if so, return it.
|
||||||
|
"""
|
||||||
|
cursor = QTextCursor(self)
|
||||||
|
cursor.setPosition(pos)
|
||||||
|
block = cursor.block()
|
||||||
|
data = block.userData()
|
||||||
|
if block.isValid() and isinstance(data, TextBlockData):
|
||||||
|
text = block.text()
|
||||||
|
check = pos - block.position()
|
||||||
|
if check >= 0:
|
||||||
|
for cPos, cLen in data.spellErrors:
|
||||||
|
cEnd = cPos + cLen
|
||||||
|
if cPos <= check <= cEnd:
|
||||||
|
word = text[cPos:cEnd]
|
||||||
|
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
|
||||||
|
return "", -1, -1, []
|
||||||
|
|
||||||
|
##
|
||||||
|
# Public Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
|
def setSpellCheckState(self, state: bool) -> None:
|
||||||
|
"""Set the spell check state of the syntax highlighter."""
|
||||||
|
self._syntax.setSpellCheck(state)
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class GuiTextDocument
|
||||||
@@ -78,10 +78,11 @@ class GuiMainMenu(QMenuBar):
|
|||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Update Menu on Settings Changed
|
# Public Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
def setSpellCheck(self, state: bool) -> None:
|
@pyqtSlot(bool)
|
||||||
|
def setSpellCheckState(self, state: bool) -> None:
|
||||||
"""Forward spell check check state to its action."""
|
"""Forward spell check check state to its action."""
|
||||||
self.aSpellCheck.setChecked(state)
|
self.aSpellCheck.setChecked(state)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from time import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
|
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
|
||||||
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence
|
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
|
QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
|
||||||
@@ -95,7 +95,6 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
logger.debug("Create: GUI")
|
logger.debug("Create: GUI")
|
||||||
self.setObjectName("GuiMain")
|
self.setObjectName("GuiMain")
|
||||||
self.threadPool = QThreadPool(self)
|
|
||||||
|
|
||||||
# System Info
|
# System Info
|
||||||
# ===========
|
# ===========
|
||||||
@@ -153,7 +152,7 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
# Project Tree View
|
# Project Tree View
|
||||||
self.treePane = QWidget(self)
|
self.treePane = QWidget(self)
|
||||||
self.treeBox = QVBoxLayout(self)
|
self.treeBox = QVBoxLayout()
|
||||||
self.treeBox.setContentsMargins(0, 0, 0, 0)
|
self.treeBox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.treeBox.setSpacing(mPx)
|
self.treeBox.setSpacing(mPx)
|
||||||
self.treeBox.addWidget(self.projStack)
|
self.treeBox.addWidget(self.projStack)
|
||||||
@@ -223,7 +222,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.rebuildTrees()
|
self.rebuildTrees()
|
||||||
|
|
||||||
# Assemble Main Window Elements
|
# Assemble Main Window Elements
|
||||||
self.mainBox = QHBoxLayout(self)
|
self.mainBox = QHBoxLayout()
|
||||||
self.mainBox.addWidget(self.sideBar)
|
self.mainBox.addWidget(self.sideBar)
|
||||||
self.mainBox.addWidget(self.mainStack)
|
self.mainBox.addWidget(self.mainStack)
|
||||||
self.mainBox.setContentsMargins(0, 0, 0, 0)
|
self.mainBox.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -267,6 +266,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
||||||
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
||||||
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
|
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
|
||||||
|
self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
|
||||||
|
|
||||||
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
||||||
|
|
||||||
@@ -621,10 +621,10 @@ class GuiMain(QMainWindow):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if nHandle is not None:
|
if nHandle is not None:
|
||||||
self.openDocument(nHandle, tLine=0, doScroll=True)
|
self.openDocument(nHandle, tLine=1, doScroll=True)
|
||||||
return True
|
return True
|
||||||
elif wrapAround:
|
elif wrapAround:
|
||||||
self.openDocument(fHandle, tLine=0, doScroll=True)
|
self.openDocument(fHandle, tLine=1, doScroll=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
+13
-2
@@ -29,7 +29,7 @@ from time import time
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox, QWidget
|
from PyQt5.QtWidgets import QMessageBox, QWidget
|
||||||
|
|
||||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||||
@@ -55,14 +55,19 @@ class SharedData(QObject):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
# Objects
|
||||||
self._gui = None
|
self._gui = None
|
||||||
self._theme = None
|
self._theme = None
|
||||||
self._project = None
|
self._project = None
|
||||||
self._spelling = None
|
self._spelling = None
|
||||||
|
|
||||||
|
# Settings
|
||||||
self._lockedBy = None
|
self._lockedBy = None
|
||||||
self._alert = None
|
self._alert = None
|
||||||
self._idleTime = 0.0
|
self._idleTime = 0.0
|
||||||
self._idleRefTime = time()
|
self._idleRefTime = time()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -129,7 +134,8 @@ class SharedData(QObject):
|
|||||||
self._gui = gui
|
self._gui = gui
|
||||||
self._theme = theme
|
self._theme = theme
|
||||||
self._resetProject()
|
self._resetProject()
|
||||||
logger.debug("SharedData instance initialised")
|
logger.debug("Ready: SharedData")
|
||||||
|
logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
|
||||||
return
|
return
|
||||||
|
|
||||||
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
|
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
|
||||||
@@ -199,6 +205,11 @@ class SharedData(QObject):
|
|||||||
self.projectStatusChanged.emit(state)
|
self.projectStatusChanged.emit(state)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
|
||||||
|
"""Queue a runnable in the application thread pool."""
|
||||||
|
QThreadPool.globalInstance().start(runnable, priority=priority)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Alert Boxes
|
# Alert Boxes
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
%%~name: Making a Scene
|
%%~name: Making a Scene
|
||||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
|
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
|
||||||
%%~date: Unknown/2023-08-25 16:56:11
|
%%~date: Unknown/2023-09-07 19:00:10
|
||||||
### Making a Scene
|
### Making a Scene
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
@@ -15,7 +15,7 @@ Each paragraph in the scene is separated by a blank line. The text supports mini
|
|||||||
|
|
||||||
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
|
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
|
||||||
|
|
||||||
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings.
|
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
|
||||||
|
|
||||||
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.
|
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-01 20:48:55">
|
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-07 19:14:50">
|
||||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1522" autoCount="237" editTime="75353">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1531" autoCount="238" editTime="75619">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<title>Sample Project</title>
|
<title>Sample Project</title>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
@@ -58,27 +58,27 @@
|
|||||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="66" />
|
<meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="465" />
|
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="531" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="310" />
|
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="0" />
|
||||||
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
|
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
|
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="0" />
|
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="1940" />
|
||||||
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
|
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="188" />
|
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="356" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="0" />
|
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="237" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
|
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="104" />
|
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="387" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
|
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
|
||||||
@@ -102,11 +102,11 @@
|
|||||||
<name status="sf12341" import="ia857f0">Main Characters</name>
|
<name status="sf12341" import="ia857f0">Main Characters</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="24" />
|
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="65" />
|
||||||
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
|
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
|
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="25" />
|
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="71" />
|
||||||
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
|
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
|
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
|
||||||
@@ -114,15 +114,15 @@
|
|||||||
<name status="sf12341" import="ia857f0">Locations</name>
|
<name status="sf12341" import="ia857f0">Locations</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="20" />
|
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="111" />
|
||||||
<name status="sf12341" import="i56be10" active="yes">Earth</name>
|
<name status="sf12341" import="i56be10" active="yes">Earth</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="133" />
|
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="135" />
|
||||||
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
|
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="45" />
|
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="62" />
|
||||||
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
|
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
|
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
%%~name: New Scene
|
%%~name: New Scene
|
||||||
%%~path: 000000000000d/000000000000f
|
%%~path: 000000000000d/000000000000f
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: fd5dc2f0c9767cb124b1bf2300d7a33b7780045e
|
%%~hash: 2a863dc53e09b0b1b0294ae7ea001853dddd596d
|
||||||
%%~date: 2023-08-25 18:08:01/2023-08-25 18:08:04
|
%%~date: 2023-10-17 20:52:56/2023-10-17 20:53:01
|
||||||
# Novel
|
# Novel
|
||||||
|
|
||||||
## Chapter
|
## Chapter
|
||||||
@@ -54,3 +54,5 @@ But don’t add a double space : See?
|
|||||||
|
|
||||||
>>‘Right-aligned text’
|
>>‘Right-aligned text’
|
||||||
|
|
||||||
|
Some text with tesst in it.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:23:02">
|
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-10-17 20:52:34">
|
||||||
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
|
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4">
|
||||||
<name>New Project</name>
|
<name>New Project</name>
|
||||||
<title>New Novel</title>
|
<title>New Novel</title>
|
||||||
<author>Jane Doe</author>
|
<author>Jane Doe</author>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content items="11" novelWords="136" notesWords="27">
|
<content items="11" novelWords="142" notesWords="27">
|
||||||
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||||
<meta expanded="yes" />
|
<meta expanded="yes" />
|
||||||
<name status="s000000" import="i000004">Novel</name>
|
<name status="s000000" import="i000004">Novel</name>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H1" charCount="781" wordCount="129" paraCount="14" cursorPos="1010" />
|
<meta expanded="no" heading="H1" charCount="808" wordCount="135" paraCount="15" cursorPos="1026" />
|
||||||
<name status="s000000" import="i000004" active="yes">New Scene</name>
|
<name status="s000000" import="i000004" active="yes">New Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import pytest
|
|||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
from tools import C, buildTestProject
|
from tools import C, buildTestProject
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import QThreadPool, Qt
|
||||||
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
|
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
|
||||||
from PyQt5.QtWidgets import QAction, qApp
|
from PyQt5.QtWidgets import QAction, qApp
|
||||||
|
|
||||||
@@ -1100,13 +1100,14 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._objID = None
|
self._objID = None
|
||||||
|
|
||||||
def start(self, runObj):
|
def start(self, runObj, priority=0):
|
||||||
self._objID = id(runObj)
|
self._objID = id(runObj)
|
||||||
|
|
||||||
def objectID(self):
|
def objectID(self):
|
||||||
return self._objID
|
return self._objID
|
||||||
|
|
||||||
nwGUI.threadPool = MockThreadPool()
|
threadPool = MockThreadPool()
|
||||||
|
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
|
||||||
nwGUI.docEditor.wcTimerDoc.blockSignals(True)
|
nwGUI.docEditor.wcTimerDoc.blockSignals(True)
|
||||||
nwGUI.docEditor.wcTimerSel.blockSignals(True)
|
nwGUI.docEditor.wcTimerSel.blockSignals(True)
|
||||||
|
|
||||||
@@ -1145,7 +1146,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
|
|
||||||
# Run the full word counter
|
# Run the full word counter
|
||||||
nwGUI.docEditor._runDocCounter()
|
nwGUI.docEditor._runDocCounter()
|
||||||
assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
|
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
|
||||||
|
|
||||||
nwGUI.docEditor.wCounterDoc.run()
|
nwGUI.docEditor.wCounterDoc.run()
|
||||||
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
|
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
|
||||||
@@ -1161,7 +1162,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
|
|
||||||
# Run the selection word counter
|
# Run the selection word counter
|
||||||
nwGUI.docEditor._runSelCounter()
|
nwGUI.docEditor._runSelCounter()
|
||||||
assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
|
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
|
||||||
|
|
||||||
nwGUI.docEditor.wCounterSel.run()
|
nwGUI.docEditor.wCounterSel.run()
|
||||||
# nwGUI.docEditor._updateSelCounts(cC, wC, pC)
|
# nwGUI.docEditor._updateSelCounts(cC, wC, pC)
|
||||||
|
|||||||
+164
-131
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
@@ -28,7 +29,7 @@ from tools import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QDialog, QMessageBox, QInputDialog
|
from PyQt5.QtWidgets import QDialog, QMenu, QMessageBox, QInputDialog
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.enum import nwItemType, nwView, nwWidget
|
from novelwriter.enum import nwItemType, nwView, nwWidget
|
||||||
@@ -47,8 +48,7 @@ KEY_DELAY = 1
|
|||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiMain_ProjectBlocker(nwGUI):
|
def testGuiMain_ProjectBlocker(nwGUI):
|
||||||
"""Test the blocking of features when there's no project open.
|
"""Test the blocking of features when there's no project open."""
|
||||||
"""
|
|
||||||
# Test no-project blocking
|
# Test no-project blocking
|
||||||
assert nwGUI.closeProject() is True
|
assert nwGUI.closeProject() is True
|
||||||
assert nwGUI.saveProject() is False
|
assert nwGUI.saveProject() is False
|
||||||
@@ -254,20 +254,25 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
|
# Text Editor
|
||||||
|
# ===========
|
||||||
|
|
||||||
|
docEditor: GuiDocEditor = nwGUI.docEditor
|
||||||
|
|
||||||
# Type something into the document
|
# Type something into the document
|
||||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
||||||
for c in "# Jane Doe":
|
for c in "# Jane Doe":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@tag: Jane":
|
for c in "@tag: Jane":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "This is a file about Jane.":
|
for c in "This is a file about Jane.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Add a Plot File
|
# Add a Plot File
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
@@ -278,18 +283,18 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
|
|
||||||
# Type something into the document
|
# Type something into the document
|
||||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
||||||
for c in "# Main Plot":
|
for c in "# Main Plot":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@tag: MainPlot":
|
for c in "@tag: MainPlot":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "This is a file detailing the main plot.":
|
for c in "This is a file detailing the main plot.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Add a World File
|
# Add a World File
|
||||||
nwGUI.switchFocus(nwWidget.TREE)
|
nwGUI.switchFocus(nwWidget.TREE)
|
||||||
@@ -299,24 +304,24 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
assert nwGUI.openSelectedItem()
|
assert nwGUI.openSelectedItem()
|
||||||
|
|
||||||
# Add Some Text
|
# Add Some Text
|
||||||
nwGUI.docEditor.replaceText("Hello World!")
|
docEditor.replaceText("Hello World!")
|
||||||
assert nwGUI.docEditor.getText() == "Hello World!"
|
assert docEditor.getText() == "Hello World!"
|
||||||
nwGUI.docEditor.replaceText("")
|
docEditor.replaceText("")
|
||||||
|
|
||||||
# Type something into the document
|
# Type something into the document
|
||||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
||||||
for c in "# Main Location":
|
for c in "# Main Location":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@tag: Home":
|
for c in "@tag: Home":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "This is a file describing Jane's home.":
|
for c in "This is a file describing Jane's home.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Trigger autosaves before making more changes
|
# Trigger autosaves before making more changes
|
||||||
nwGUI._autoSaveDocument()
|
nwGUI._autoSaveDocument()
|
||||||
@@ -332,67 +337,67 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
|
|
||||||
# Type something into the document
|
# Type something into the document
|
||||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||||
qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
|
||||||
for c in "# Novel":
|
for c in "# Novel":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "## Chapter":
|
for c in "## Chapter":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "@pov: Jane":
|
for c in "@pov: Jane":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@plot: MainPlot":
|
for c in "@plot: MainPlot":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "### Scene":
|
for c in "### Scene":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "% How about a comment?":
|
for c in "% How about a comment?":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@pov: Jane":
|
for c in "@pov: Jane":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@plot: MainPlot":
|
for c in "@plot: MainPlot":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
for c in "@location: Home":
|
for c in "@location: Home":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "#### Some Section":
|
for c in "#### Some Section":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "@char: Jane":
|
for c in "@char: Jane":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "This is a paragraph of nonsense text.":
|
for c in "This is a paragraph of nonsense text.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Don't allow Shift+Enter to insert a line separator (issue #1150)
|
# Don't allow Shift+Enter to insert a line separator (issue #1150)
|
||||||
for c in "This is another paragraph":
|
for c in "This is another paragraph":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
|
||||||
for c in "with a line separator in it.":
|
for c in "with a line separator in it.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Auto-Replace
|
# Auto-Replace
|
||||||
# ============
|
# ============
|
||||||
@@ -401,111 +406,139 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
"This is another paragraph of much longer nonsense text. "
|
"This is another paragraph of much longer nonsense text. "
|
||||||
"It is in fact 1 very very NONSENSICAL nonsense text! "
|
"It is in fact 1 very very NONSENSICAL nonsense text! "
|
||||||
):
|
):
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ":
|
for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "Isn't that nice? ":
|
for c in "Isn't that nice? ":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "Ellipsis? Not a problem either ... ":
|
for c in "Ellipsis? Not a problem either ... ":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
for c in "How about three hyphens - -":
|
for c in "How about three hyphens - -":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Left, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Left, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Right, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Right, delay=KEY_DELAY)
|
||||||
for c in "- for long dash? It works too.":
|
for c in "- for long dash? It works too.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "\"Full line double quoted text.\"":
|
for c in "\"Full line double quoted text.\"":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "'Full line single quoted text.'":
|
for c in "'Full line single quoted text.'":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
# Insert spaces before and after quotes
|
# Insert spaces before and after quotes
|
||||||
nwGUI.docEditor._typPadBefore = "\u201d"
|
docEditor._typPadBefore = "\u201d"
|
||||||
nwGUI.docEditor._typPadAfter = "\u201c"
|
docEditor._typPadAfter = "\u201c"
|
||||||
|
|
||||||
for c in "Some \"double quoted text with spaces padded\".":
|
for c in "Some \"double quoted text with spaces padded\".":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
nwGUI.docEditor._typPadBefore = ""
|
docEditor._typPadBefore = ""
|
||||||
nwGUI.docEditor._typPadAfter = ""
|
docEditor._typPadAfter = ""
|
||||||
|
|
||||||
# Insert spaces before colon, but ignore tags and synopsis
|
# Insert spaces before colon, but ignore tags and synopsis
|
||||||
nwGUI.docEditor._typPadBefore = ":"
|
docEditor._typPadBefore = ":"
|
||||||
|
|
||||||
for c in "@object: NoSpaceAdded":
|
for c in "@object: NoSpaceAdded":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "% synopsis: No space before this colon.":
|
for c in "% synopsis: No space before this colon.":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "Add space before this colon: See?":
|
for c in "Add space before this colon: See?":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "But don't add a double space : See?":
|
for c in "But don't add a double space : See?":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
nwGUI.docEditor._typPadBefore = ""
|
docEditor._typPadBefore = ""
|
||||||
|
|
||||||
# Indent and Align
|
# Indent and Align
|
||||||
# ================
|
# ================
|
||||||
|
|
||||||
for c in "\t\"Tab-indented text\"":
|
for c in "\t\"Tab-indented text\"":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in ">\"Paragraph-indented text\"":
|
for c in ">\"Paragraph-indented text\"":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in ">>\"Right-aligned text\"":
|
for c in ">>\"Right-aligned text\"":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in "\t'Tab-indented text'":
|
for c in "\t'Tab-indented text'":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in ">'Paragraph-indented text'":
|
for c in ">'Paragraph-indented text'":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
for c in ">>'Right-aligned text'":
|
for c in ">>'Right-aligned text'":
|
||||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
nwGUI.docEditor.wCounterDoc.run()
|
docEditor.wCounterDoc.run()
|
||||||
|
|
||||||
|
# Spell Checking
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
for c in "Some text with tesst in it.":
|
||||||
|
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||||
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
|
currPos = docEditor.getCursorPosition()
|
||||||
|
assert docEditor._qDocument.spellErrorAtPos(currPos) == ("", -1, -1, [])
|
||||||
|
|
||||||
|
errPos = currPos - 13
|
||||||
|
if not sys.platform.startswith("win32"):
|
||||||
|
# Skip on Windows as spell checking is off there
|
||||||
|
word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos)
|
||||||
|
assert word == "tesst"
|
||||||
|
assert cPos == 15
|
||||||
|
assert cLen == 5
|
||||||
|
assert "test" in suggest
|
||||||
|
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(QMenu, "exec_", lambda *a: None)
|
||||||
|
docEditor.setCursorPosition(errPos)
|
||||||
|
docEditor._openSpellContext()
|
||||||
|
|
||||||
|
# Check Files
|
||||||
|
# ===========
|
||||||
|
|
||||||
# Save the document
|
# Save the document
|
||||||
assert nwGUI.docEditor.docChanged
|
assert docEditor.docChanged
|
||||||
assert nwGUI.saveDocument()
|
assert nwGUI.saveDocument()
|
||||||
assert not nwGUI.docEditor.docChanged
|
assert not docEditor.docChanged
|
||||||
nwGUI.rebuildIndex()
|
nwGUI.rebuildIndex()
|
||||||
|
|
||||||
# Open and view the edited document
|
# Open and view the edited document
|
||||||
|
|||||||
Reference in New Issue
Block a user