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
##
# Global config singleton
# Global config and data singletons
CONFIG = Config()
SHARED = SharedData()
+30 -34
View File
@@ -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}>&nbsp;</p>\n")
lines.append(f"<p class='skip'{hStyle}>&nbsp;</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
View File
@@ -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
View File
@@ -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
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.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
+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
##
# 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
+6 -6
View File
@@ -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
View File
@@ -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
##
+3 -3
View File
@@ -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, 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.
+14 -14
View File
@@ -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 dont 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">
+6 -5
View File
@@ -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
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/>.
"""
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