Change editor to true plain text (#1525)
This commit is contained in:
@@ -73,7 +73,7 @@ logger = logging.getLogger(__name__)
|
||||
# Main Program
|
||||
##
|
||||
|
||||
# Global config singleton
|
||||
# Global config and data singletons
|
||||
CONFIG = Config()
|
||||
SHARED = SharedData()
|
||||
|
||||
|
||||
+30
-34
@@ -111,7 +111,7 @@ class ToHtml(Tokenizer):
|
||||
|
||||
def getFullResultSize(self) -> int:
|
||||
"""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:
|
||||
"""Extend the auto-replace to also properly encode some unicode
|
||||
@@ -122,9 +122,7 @@ class ToHtml(Tokenizer):
|
||||
return
|
||||
|
||||
def doConvert(self) -> None:
|
||||
"""Convert the list of text tokens into a HTML document saved
|
||||
to _result.
|
||||
"""
|
||||
"""Convert the list of text tokens into an HTML document."""
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
htmlTags = { # HTML4 + CSS2 (for Qt)
|
||||
self.FMT_B_B: "<b>",
|
||||
@@ -160,9 +158,9 @@ class ToHtml(Tokenizer):
|
||||
|
||||
self._result = ""
|
||||
|
||||
thisPar = []
|
||||
parStyle = None
|
||||
tmpResult = []
|
||||
para = []
|
||||
pStyle = None
|
||||
lines = []
|
||||
|
||||
for tType, tLine, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
@@ -231,69 +229,67 @@ class ToHtml(Tokenizer):
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if parStyle is None:
|
||||
parStyle = ""
|
||||
if len(thisPar) > 1 and self._cssStyles:
|
||||
parClass = " class='break'"
|
||||
if pStyle is None:
|
||||
pStyle = ""
|
||||
if len(para) > 1 and self._cssStyles:
|
||||
pClass = " class='break'"
|
||||
else:
|
||||
parClass = ""
|
||||
if len(thisPar) > 0:
|
||||
tTemp = "<br/>".join(thisPar)
|
||||
tmpResult.append(f"<p{parClass+parStyle}>{tTemp.rstrip()}</p>\n")
|
||||
thisPar = []
|
||||
parStyle = None
|
||||
pClass = ""
|
||||
if len(para) > 0:
|
||||
tTemp = "<br/>".join(para)
|
||||
lines.append(f"<p{pClass+pStyle}>{tTemp.rstrip()}</p>\n")
|
||||
para = []
|
||||
pStyle = None
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
tmpResult.append(f"<p class='skip'{hStyle}> </p>\n")
|
||||
lines.append(f"<p class='skip'{hStyle}> </p>\n")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
if parStyle is None:
|
||||
parStyle = hStyle
|
||||
if pStyle is None:
|
||||
pStyle = hStyle
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
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:
|
||||
tmpResult.append(self._formatSynopsis(tText))
|
||||
lines.append(self._formatSynopsis(tText))
|
||||
|
||||
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:
|
||||
tTemp = f"<p{hStyle}>{self._formatKeywords(tText)}</p>\n"
|
||||
tmpResult.append(tTemp)
|
||||
|
||||
self._result = "".join(tmpResult)
|
||||
tmpResult = []
|
||||
lines.append(tTemp)
|
||||
|
||||
self._result = "".join(lines)
|
||||
if self._genMode != self.M_PREVIEW:
|
||||
self._fullHTML.append(self._result)
|
||||
|
||||
|
||||
+25
-28
@@ -65,10 +65,12 @@ class ToMarkdown(Tokenizer):
|
||||
##
|
||||
|
||||
def setStandardMarkdown(self) -> None:
|
||||
"""Set the converter to use standard Markdown formatting."""
|
||||
self._genMode = self.M_STD
|
||||
return
|
||||
|
||||
def setGitHubMarkdown(self) -> None:
|
||||
"""Set the converter to use GitHub Markdown formatting."""
|
||||
self._genMode = self.M_GH
|
||||
return
|
||||
|
||||
@@ -78,12 +80,10 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
def getFullResultSize(self) -> int:
|
||||
"""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:
|
||||
"""Convert the list of text tokens into a HTML document saved
|
||||
to theResult.
|
||||
"""
|
||||
"""Convert the list of text tokens into a Markdown document."""
|
||||
if self._genMode == self.M_STD:
|
||||
# Standard
|
||||
mdTags = {
|
||||
@@ -107,68 +107,65 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
self._result = ""
|
||||
|
||||
thisPar = []
|
||||
tmpResult = []
|
||||
para = []
|
||||
lines = []
|
||||
|
||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
tTemp = (" \n".join(thisPar)).rstrip(" ")
|
||||
tmpResult.append(f"{tTemp}\n\n")
|
||||
thisPar = []
|
||||
if len(para) > 0:
|
||||
tTemp = (" \n".join(para)).rstrip(" ")
|
||||
lines.append(f"{tTemp}\n\n")
|
||||
para = []
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"# {tHead}\n\n")
|
||||
lines.append(f"# {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_UNNUM:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"## {tHead}\n\n")
|
||||
lines.append(f"## {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"# {tHead}\n\n")
|
||||
lines.append(f"# {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"## {tHead}\n\n")
|
||||
lines.append(f"## {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"### {tHead}\n\n")
|
||||
lines.append(f"### {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
tmpResult.append(f"#### {tHead}\n\n")
|
||||
lines.append(f"#### {tHead}\n\n")
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
tmpResult.append("%s\n\n" % tText)
|
||||
lines.append(f"{tText}\n\n")
|
||||
|
||||
elif tType == self.T_SKIP:
|
||||
tmpResult.append("\n\n\n")
|
||||
lines.append("\n\n\n")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
||||
thisPar.append(tTemp.rstrip())
|
||||
para.append(tTemp.rstrip())
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
locName = self._localLookup("Synopsis")
|
||||
tmpResult.append(f"**{locName}:** {tText}\n\n")
|
||||
label = self._localLookup("Synopsis")
|
||||
lines.append(f"**{label}:** {tText}\n\n")
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
locName = self._localLookup("Comment")
|
||||
tmpResult.append(f"**{locName}:** {tText}\n\n")
|
||||
label = self._localLookup("Comment")
|
||||
lines.append(f"**{label}:** {tText}\n\n")
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
tmpResult.append(self._formatKeywords(tText, tStyle))
|
||||
|
||||
self._result = "".join(tmpResult)
|
||||
tmpResult = []
|
||||
lines.append(self._formatKeywords(tText, tStyle))
|
||||
|
||||
self._result = "".join(lines)
|
||||
self._fullMD.append(self._result)
|
||||
|
||||
return
|
||||
|
||||
+60
-60
@@ -27,10 +27,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from datetime import datetime
|
||||
|
||||
@@ -394,9 +394,9 @@ class ToOdt(Tokenizer):
|
||||
self.FMT_D_E: "s_", # Strikethrough close format
|
||||
}
|
||||
|
||||
thisPar = []
|
||||
thisFmt = []
|
||||
parStyle = None
|
||||
fmt = []
|
||||
para = []
|
||||
pStyle = None
|
||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
# Styles
|
||||
@@ -429,20 +429,20 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Process Text Types
|
||||
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:
|
||||
parStyle.setTextAlign("left")
|
||||
pStyle.setTextAlign("left")
|
||||
|
||||
if len(thisPar) > 0 and parStyle is not None:
|
||||
tTemp = "\n".join(thisPar)
|
||||
fTemp = " ".join(thisFmt)
|
||||
if len(para) > 0 and pStyle is not None:
|
||||
tTemp = "\n".join(para)
|
||||
fTemp = " ".join(fmt)
|
||||
tTxt = tTemp.rstrip()
|
||||
tFmt = fTemp[:len(tTxt)]
|
||||
self._addTextPar("Text_20_body", parStyle, tTxt, tFmt=tFmt)
|
||||
self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
|
||||
|
||||
thisPar = []
|
||||
thisFmt = []
|
||||
parStyle = None
|
||||
fmt = []
|
||||
para = []
|
||||
pStyle = None
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
@@ -475,8 +475,8 @@ class ToOdt(Tokenizer):
|
||||
self._addTextPar("Separator", oStyle, "")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
if parStyle is None:
|
||||
parStyle = oStyle
|
||||
if pStyle is None:
|
||||
pStyle = oStyle
|
||||
|
||||
tFmt = " "*len(tText)
|
||||
for xPos, xLen, xFmt in tFormat:
|
||||
@@ -484,8 +484,8 @@ class ToOdt(Tokenizer):
|
||||
|
||||
tTxt = tText.rstrip()
|
||||
tFmt = tFmt[:len(tTxt)]
|
||||
thisPar.append(tTxt)
|
||||
thisFmt.append(tFmt)
|
||||
para.append(tTxt)
|
||||
fmt.append(tFmt)
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
tTemp, fTemp = self._formatSynopsis(tText)
|
||||
@@ -501,8 +501,8 @@ class ToOdt(Tokenizer):
|
||||
|
||||
return
|
||||
|
||||
def closeDocument(self):
|
||||
"""Return the serialised XML document"""
|
||||
def closeDocument(self) -> None:
|
||||
"""Pack the styles of the XML document."""
|
||||
# Build the auto-generated styles
|
||||
for styleName, styleObj in self._autoPara.values():
|
||||
styleObj.packXML(self._xAuto, styleName)
|
||||
@@ -510,7 +510,7 @@ class ToOdt(Tokenizer):
|
||||
styleObj.packXML(self._xAuto, styleName)
|
||||
return
|
||||
|
||||
def saveFlatXML(self, path: str | Path):
|
||||
def saveFlatXML(self, path: str | Path) -> None:
|
||||
"""Save the data to an .fodt file."""
|
||||
with open(path, mode="wb") as fObj:
|
||||
xml = ET.ElementTree(self._dFlat)
|
||||
@@ -519,7 +519,7 @@ class ToOdt(Tokenizer):
|
||||
logger.info("Wrote file: %s", path)
|
||||
return
|
||||
|
||||
def saveOpenDocText(self, path: str | Path):
|
||||
def saveOpenDocText(self, path: str | Path) -> None:
|
||||
"""Save the data to an .odt file."""
|
||||
mMani = _mkTag("manifest", "manifest")
|
||||
mVers = _mkTag("manifest", "version")
|
||||
@@ -718,9 +718,9 @@ class ToOdt(Tokenizer):
|
||||
|
||||
return newName
|
||||
|
||||
def _emToCm(self, emVal: float) -> str:
|
||||
def _emToCm(self, value: float) -> str:
|
||||
"""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
|
||||
@@ -1193,56 +1193,56 @@ class ODTParagraphStyle:
|
||||
# Methods
|
||||
##
|
||||
|
||||
def checkNew(self, refStyle: ODTParagraphStyle) -> bool:
|
||||
def checkNew(self, style: ODTParagraphStyle) -> bool:
|
||||
"""Check if there are new settings in refStyle that differ from
|
||||
those in the current object.
|
||||
"""
|
||||
for aName, (_, aVal) in refStyle._mAttr.items():
|
||||
if aVal is not None and aVal != self._mAttr[aName][1]:
|
||||
for name, (_, aVal) in style._mAttr.items():
|
||||
if aVal is not None and aVal != self._mAttr[name][1]:
|
||||
return True
|
||||
for aName, (_, aVal) in refStyle._pAttr.items():
|
||||
if aVal is not None and aVal != self._pAttr[aName][1]:
|
||||
for name, (_, aVal) in style._pAttr.items():
|
||||
if aVal is not None and aVal != self._pAttr[name][1]:
|
||||
return True
|
||||
for aName, (_, aVal) in refStyle._tAttr.items():
|
||||
if aVal is not None and aVal != self._tAttr[aName][1]:
|
||||
for name, (_, aVal) in style._tAttr.items():
|
||||
if aVal is not None and aVal != self._tAttr[name][1]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getID(self) -> str:
|
||||
"""Generate a unique ID from the settings."""
|
||||
theString = (
|
||||
string = (
|
||||
f"Paragraph:Main:{str(self._mAttr)}:"
|
||||
f"Paragraph:Para:{str(self._pAttr)}:"
|
||||
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:
|
||||
"""Pack the content into an xml element."""
|
||||
theAttr = {}
|
||||
theAttr[_mkTag("style", "name")] = name
|
||||
theAttr[_mkTag("style", "family")] = "paragraph"
|
||||
attr = {}
|
||||
attr[_mkTag("style", "name")] = name
|
||||
attr[_mkTag("style", "family")] = "paragraph"
|
||||
for aName, (aNm, aVal) in self._mAttr.items():
|
||||
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():
|
||||
if aVal is not None:
|
||||
theAttr[_mkTag(aNm, aName)] = aVal
|
||||
attr[_mkTag(aNm, aName)] = aVal
|
||||
|
||||
if theAttr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
|
||||
if attr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=attr)
|
||||
|
||||
theAttr = {}
|
||||
attr = {}
|
||||
for aName, (aNm, aVal) in self._tAttr.items():
|
||||
if aVal is not None:
|
||||
theAttr[_mkTag(aNm, aName)] = aVal
|
||||
attr[_mkTag(aNm, aName)] = aVal
|
||||
|
||||
if theAttr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
||||
if attr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
|
||||
|
||||
return
|
||||
|
||||
@@ -1307,18 +1307,18 @@ class ODTTextStyle:
|
||||
|
||||
def packXML(self, xParent: ET.Element, name: str) -> None:
|
||||
"""Pack the content into an xml element."""
|
||||
theAttr = {}
|
||||
theAttr[_mkTag("style", "name")] = name
|
||||
theAttr[_mkTag("style", "family")] = "text"
|
||||
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
|
||||
attr = {}
|
||||
attr[_mkTag("style", "name")] = name
|
||||
attr[_mkTag("style", "family")] = "text"
|
||||
xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
|
||||
|
||||
theAttr = {}
|
||||
attr = {}
|
||||
for aName, (aNm, aVal) in self._tAttr.items():
|
||||
if aVal is not None:
|
||||
theAttr[_mkTag(aNm, aName)] = aVal
|
||||
attr[_mkTag(aNm, aName)] = aVal
|
||||
|
||||
if theAttr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
|
||||
if attr:
|
||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
|
||||
|
||||
return
|
||||
|
||||
@@ -1368,17 +1368,17 @@ class XMLParagraph:
|
||||
|
||||
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
|
||||
the time in order to be able to process line breaks, tabs and
|
||||
spaces separately. Multiple spaces are concatenated into a
|
||||
single tag, and must therefore be processed separately.
|
||||
"""
|
||||
tText = stripEscape(tText)
|
||||
text = stripEscape(text)
|
||||
nSpaces = 0
|
||||
self._rawTxt += tText
|
||||
self._rawTxt += text
|
||||
|
||||
for c in tText:
|
||||
for c in text:
|
||||
if c == " ":
|
||||
nSpaces += 1
|
||||
continue
|
||||
@@ -1433,17 +1433,17 @@ class XMLParagraph:
|
||||
|
||||
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
|
||||
closed since we do not allow nested spans (like Libre Office).
|
||||
Therefore we return to the root element level when we're done
|
||||
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.tail = "" # Defaults to None
|
||||
self._nState = X_SPAN_TEXT
|
||||
self.appendText(tText)
|
||||
self.appendText(text)
|
||||
self._nState = X_ROOT_TAIL
|
||||
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.QtGui import (
|
||||
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush, QTextDocument
|
||||
QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData,
|
||||
QTextCharFormat, QTextDocument
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
@@ -38,6 +39,9 @@ from novelwriter.constants import nwRegEx, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
|
||||
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||
|
||||
|
||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
@@ -51,9 +55,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
logger.debug("Create: GuiDocHighlighter")
|
||||
|
||||
self._tItem = None
|
||||
self._tHandle = None
|
||||
self._spellCheck = False
|
||||
self._spellRx = QRegularExpression()
|
||||
|
||||
self._hRules: list[tuple[str, dict]] = []
|
||||
self._hStyles: dict[str, QTextCharFormat] = {}
|
||||
@@ -79,11 +83,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def spellCheck(self) -> bool:
|
||||
"""Check if spell checking is enabled."""
|
||||
return self._spellCheck
|
||||
|
||||
def initHighlighter(self) -> None:
|
||||
"""Initialise the syntax highlighter, setting all the colour
|
||||
rules and building the RegExes.
|
||||
@@ -227,13 +226,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||
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
|
||||
|
||||
##
|
||||
@@ -248,6 +240,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Set the handle of the currently highlighted document."""
|
||||
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
|
||||
|
||||
##
|
||||
@@ -284,27 +281,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
if text.startswith("@"): # Keywords and commands
|
||||
self.setCurrentBlockState(self.BLOCK_META)
|
||||
pIndex = SHARED.project.index
|
||||
tItem = SHARED.project.tree[self._tHandle]
|
||||
if tItem is None:
|
||||
return
|
||||
|
||||
isValid, theBits, thePos = pIndex.scanThis(text)
|
||||
isGood = pIndex.checkThese(theBits, tItem)
|
||||
if isValid:
|
||||
for n, theBit in enumerate(theBits):
|
||||
xPos = thePos[n]
|
||||
xLen = len(theBit)
|
||||
if isGood[n]:
|
||||
if n == 0:
|
||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||
if self._tItem:
|
||||
pIndex = SHARED.project.index
|
||||
isValid, theBits, thePos = pIndex.scanThis(text)
|
||||
isGood = pIndex.checkThese(theBits, self._tItem)
|
||||
if isValid:
|
||||
for n, theBit in enumerate(theBits):
|
||||
xPos = thePos[n]
|
||||
xLen = len(theBit)
|
||||
if isGood[n]:
|
||||
if n == 0:
|
||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||
else:
|
||||
self.setFormat(xPos, xLen, self._hStyles["value"])
|
||||
else:
|
||||
self.setFormat(xPos, xLen, self._hStyles["value"])
|
||||
else:
|
||||
kwFmt = self.format(xPos)
|
||||
kwFmt.setUnderlineColor(self._colError)
|
||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
self.setFormat(xPos, xLen, kwFmt)
|
||||
kwFmt = self.format(xPos)
|
||||
kwFmt.setUnderlineColor(self._colError)
|
||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
self.setFormat(xPos, xLen, kwFmt)
|
||||
|
||||
# We never want to run the spell checker on keyword/values,
|
||||
# so we force a return here
|
||||
@@ -382,17 +376,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
spFmt.merge(xFmt[xM])
|
||||
self.setFormat(x, 1, spFmt)
|
||||
|
||||
if not self._spellCheck:
|
||||
return
|
||||
data = self.currentBlockUserData()
|
||||
if not isinstance(data, TextBlockData):
|
||||
data = TextBlockData()
|
||||
self.setCurrentBlockUserData(data)
|
||||
|
||||
rxSpell = self._spellRx.globalMatch(text.replace("_", " "), 0)
|
||||
while rxSpell.hasNext():
|
||||
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)
|
||||
if self._spellCheck:
|
||||
for xPos, xLen in data.spellCheck(text):
|
||||
for x in range(xPos, xPos+xLen):
|
||||
spFmt = self.format(x)
|
||||
spFmt.setUnderlineColor(self._colSpell)
|
||||
@@ -437,3 +427,33 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
return charFormat
|
||||
|
||||
# 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
|
||||
|
||||
##
|
||||
# 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."""
|
||||
self.aSpellCheck.setChecked(state)
|
||||
return
|
||||
|
||||
@@ -30,7 +30,7 @@ from time import time
|
||||
from pathlib import Path
|
||||
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.QtWidgets import (
|
||||
QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
|
||||
@@ -95,7 +95,6 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.debug("Create: GUI")
|
||||
self.setObjectName("GuiMain")
|
||||
self.threadPool = QThreadPool(self)
|
||||
|
||||
# System Info
|
||||
# ===========
|
||||
@@ -153,7 +152,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Project Tree View
|
||||
self.treePane = QWidget(self)
|
||||
self.treeBox = QVBoxLayout(self)
|
||||
self.treeBox = QVBoxLayout()
|
||||
self.treeBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.treeBox.setSpacing(mPx)
|
||||
self.treeBox.addWidget(self.projStack)
|
||||
@@ -223,7 +222,7 @@ class GuiMain(QMainWindow):
|
||||
self.rebuildTrees()
|
||||
|
||||
# Assemble Main Window Elements
|
||||
self.mainBox = QHBoxLayout(self)
|
||||
self.mainBox = QHBoxLayout()
|
||||
self.mainBox.addWidget(self.sideBar)
|
||||
self.mainBox.addWidget(self.mainStack)
|
||||
self.mainBox.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -267,6 +266,7 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
||||
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
||||
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
|
||||
self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
|
||||
|
||||
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
||||
|
||||
@@ -621,10 +621,10 @@ class GuiMain(QMainWindow):
|
||||
break
|
||||
|
||||
if nHandle is not None:
|
||||
self.openDocument(nHandle, tLine=0, doScroll=True)
|
||||
self.openDocument(nHandle, tLine=1, doScroll=True)
|
||||
return True
|
||||
elif wrapAround:
|
||||
self.openDocument(fHandle, tLine=0, doScroll=True)
|
||||
self.openDocument(fHandle, tLine=1, doScroll=True)
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
+13
-2
@@ -29,7 +29,7 @@ from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
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 novelwriter.core.spellcheck import NWSpellEnchant
|
||||
@@ -55,14 +55,19 @@ class SharedData(QObject):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Objects
|
||||
self._gui = None
|
||||
self._theme = None
|
||||
self._project = None
|
||||
self._spelling = None
|
||||
|
||||
# Settings
|
||||
self._lockedBy = None
|
||||
self._alert = None
|
||||
self._idleTime = 0.0
|
||||
self._idleRefTime = time()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -129,7 +134,8 @@ class SharedData(QObject):
|
||||
self._gui = gui
|
||||
self._theme = theme
|
||||
self._resetProject()
|
||||
logger.debug("SharedData instance initialised")
|
||||
logger.debug("Ready: SharedData")
|
||||
logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
|
||||
return
|
||||
|
||||
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
|
||||
@@ -199,6 +205,11 @@ class SharedData(QObject):
|
||||
self.projectStatusChanged.emit(state)
|
||||
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
|
||||
##
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
%%~name: Making a Scene
|
||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
|
||||
%%~date: Unknown/2023-08-25 16:56:11
|
||||
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
|
||||
%%~date: Unknown/2023-09-07 19:00:10
|
||||
### Making a Scene
|
||||
|
||||
@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.”
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+14
-14
@@ -1,6 +1,6 @@
|
||||
<?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">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1522" autoCount="237" editTime="75353">
|
||||
<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="1531" autoCount="238" editTime="75619">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
@@ -58,27 +58,27 @@
|
||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
|
||||
@@ -114,15 +114,15 @@
|
||||
<name status="sf12341" import="ia857f0">Locations</name>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
%%~name: New Scene
|
||||
%%~path: 000000000000d/000000000000f
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: fd5dc2f0c9767cb124b1bf2300d7a33b7780045e
|
||||
%%~date: 2023-08-25 18:08:01/2023-08-25 18:08:04
|
||||
%%~hash: 2a863dc53e09b0b1b0294ae7ea001853dddd596d
|
||||
%%~date: 2023-10-17 20:52:56/2023-10-17 20:53:01
|
||||
# Novel
|
||||
|
||||
## Chapter
|
||||
@@ -54,3 +54,5 @@ But don’t add a double space : See?
|
||||
|
||||
>>‘Right-aligned text’
|
||||
|
||||
Some text with tesst in it.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?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">
|
||||
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
|
||||
<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="4">
|
||||
<name>New Project</name>
|
||||
<title>New Novel</title>
|
||||
<author>Jane Doe</author>
|
||||
@@ -29,7 +29,7 @@
|
||||
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
|
||||
</importance>
|
||||
</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">
|
||||
<meta expanded="yes" />
|
||||
<name status="s000000" import="i000004">Novel</name>
|
||||
@@ -47,7 +47,7 @@
|
||||
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
|
||||
@@ -24,7 +24,7 @@ import pytest
|
||||
from mocked import causeOSError
|
||||
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.QtWidgets import QAction, qApp
|
||||
|
||||
@@ -1100,13 +1100,14 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
||||
def __init__(self):
|
||||
self._objID = None
|
||||
|
||||
def start(self, runObj):
|
||||
def start(self, runObj, priority=0):
|
||||
self._objID = id(runObj)
|
||||
|
||||
def objectID(self):
|
||||
return self._objID
|
||||
|
||||
nwGUI.threadPool = MockThreadPool()
|
||||
threadPool = MockThreadPool()
|
||||
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
|
||||
nwGUI.docEditor.wcTimerDoc.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
|
||||
nwGUI.docEditor._runDocCounter()
|
||||
assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
|
||||
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
|
||||
|
||||
nwGUI.docEditor.wCounterDoc.run()
|
||||
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
|
||||
@@ -1161,7 +1162,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
||||
|
||||
# Run the selection word counter
|
||||
nwGUI.docEditor._runSelCounter()
|
||||
assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
|
||||
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
|
||||
|
||||
nwGUI.docEditor.wCounterSel.run()
|
||||
# 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/>.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from shutil import copyfile
|
||||
@@ -28,7 +29,7 @@ from tools import (
|
||||
)
|
||||
|
||||
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.enum import nwItemType, nwView, nwWidget
|
||||
@@ -47,8 +48,7 @@ KEY_DELAY = 1
|
||||
|
||||
@pytest.mark.gui
|
||||
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
|
||||
assert nwGUI.closeProject() is True
|
||||
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)
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Text Editor
|
||||
# ===========
|
||||
|
||||
docEditor: GuiDocEditor = nwGUI.docEditor
|
||||
|
||||
# Type something into the document
|
||||
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":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "@tag: Jane":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "This is a file about Jane.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
|
||||
# Add a Plot File
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
@@ -278,18 +283,18 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
|
||||
# Type something into the document
|
||||
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":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "@tag: MainPlot":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "This is a file detailing the main plot.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
|
||||
# Add a World File
|
||||
nwGUI.switchFocus(nwWidget.TREE)
|
||||
@@ -299,24 +304,24 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
assert nwGUI.openSelectedItem()
|
||||
|
||||
# Add Some Text
|
||||
nwGUI.docEditor.replaceText("Hello World!")
|
||||
assert nwGUI.docEditor.getText() == "Hello World!"
|
||||
nwGUI.docEditor.replaceText("")
|
||||
docEditor.replaceText("Hello World!")
|
||||
assert docEditor.getText() == "Hello World!"
|
||||
docEditor.replaceText("")
|
||||
|
||||
# Type something into the document
|
||||
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":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "@tag: Home":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
for c in "This is a file describing Jane's home.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
|
||||
# Trigger autosaves before making more changes
|
||||
nwGUI._autoSaveDocument()
|
||||
@@ -332,67 +337,67 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
|
||||
# Type something into the document
|
||||
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":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "## Chapter":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "@pov: Jane":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
for c in "@plot: MainPlot":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "### Scene":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "% How about a comment?":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
for c in "@pov: Jane":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
for c in "@plot: MainPlot":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
for c in "@location: Home":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "#### Some Section":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "@char: Jane":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "This is a paragraph of nonsense text.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
# Don't allow Shift+Enter to insert a line separator (issue #1150)
|
||||
for c in "This is another paragraph":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
|
||||
for c in "with a line separator in it.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
# Auto-Replace
|
||||
# ============
|
||||
@@ -401,111 +406,139 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
"This is another paragraph of much longer 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. ":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
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. ":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
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 - -":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Left, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Right, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Left, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, Qt.Key_Right, delay=KEY_DELAY)
|
||||
for c in "- for long dash? It works too.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "\"Full line double quoted text.\"":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "'Full line single quoted text.'":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
# Insert spaces before and after quotes
|
||||
nwGUI.docEditor._typPadBefore = "\u201d"
|
||||
nwGUI.docEditor._typPadAfter = "\u201c"
|
||||
docEditor._typPadBefore = "\u201d"
|
||||
docEditor._typPadAfter = "\u201c"
|
||||
|
||||
for c in "Some \"double quoted text with spaces padded\".":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
nwGUI.docEditor._typPadBefore = ""
|
||||
nwGUI.docEditor._typPadAfter = ""
|
||||
docEditor._typPadBefore = ""
|
||||
docEditor._typPadAfter = ""
|
||||
|
||||
# Insert spaces before colon, but ignore tags and synopsis
|
||||
nwGUI.docEditor._typPadBefore = ":"
|
||||
docEditor._typPadBefore = ":"
|
||||
|
||||
for c in "@object: NoSpaceAdded":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "% synopsis: No space before this colon.":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "Add space before this colon: See?":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "But don't add a double space : See?":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
nwGUI.docEditor._typPadBefore = ""
|
||||
docEditor._typPadBefore = ""
|
||||
|
||||
# Indent and Align
|
||||
# ================
|
||||
|
||||
for c in "\t\"Tab-indented text\"":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in ">\"Paragraph-indented text\"":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in ">>\"Right-aligned text\"":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in "\t'Tab-indented text'":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in ">'Paragraph-indented text'":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
for c in ">>'Right-aligned text'":
|
||||
qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
|
||||
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)
|
||||
|
||||
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
|
||||
assert nwGUI.docEditor.docChanged
|
||||
assert docEditor.docChanged
|
||||
assert nwGUI.saveDocument()
|
||||
assert not nwGUI.docEditor.docChanged
|
||||
assert not docEditor.docChanged
|
||||
nwGUI.rebuildIndex()
|
||||
|
||||
# Open and view the edited document
|
||||
|
||||
Reference in New Issue
Block a user