Change editor to true plain text (#1525)

This commit is contained in:
Veronica Berglyd Olsen
2023-10-17 21:03:36 +02:00
committed by GitHub
16 changed files with 931 additions and 823 deletions
+1 -1
View File
@@ -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
View File
@@ -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}>&nbsp;</p>\n") lines.append(f"<p class='skip'{hStyle}>&nbsp;</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
View File
@@ -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
View File
@@ -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
File diff suppressed because it is too large Load Diff
+64 -44
View File
@@ -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
+126
View File
@@ -0,0 +1,126 @@
"""
novelWriter GUI Text Document
===============================
File History:
Created: 2023-09-07 [2.2b1]
This file is a part of novelWriter
Copyright 20182023, 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
+3 -2
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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
## ##
+3 -3
View File
@@ -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, lets 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, lets 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
View File
@@ -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 dont 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">
+6 -5
View File
@@ -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
View File
@@ -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