Add a custom text document in the editor
This commit is contained in:
@@ -56,6 +56,7 @@ from novelwriter.common import minmax, transferCase
|
|||||||
from novelwriter.constants import nwKeyWords, nwUnicode
|
from novelwriter.constants import nwKeyWords, nwUnicode
|
||||||
from novelwriter.core.index import countWords
|
from novelwriter.core.index import countWords
|
||||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||||
|
from novelwriter.gui.editordocument import GuiTextDocument
|
||||||
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
|
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -120,9 +121,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._typPadBefore = ""
|
self._typPadBefore = ""
|
||||||
self._typPadAfter = ""
|
self._typPadAfter = ""
|
||||||
|
|
||||||
# Core Elements and Signals
|
# Create Custom Document
|
||||||
qDoc = self.document()
|
self._qDocument = GuiTextDocument(self)
|
||||||
qDoc.contentsChange.connect(self._docChange)
|
self.setDocument(self._qDocument)
|
||||||
|
|
||||||
|
# Connect Signals
|
||||||
|
self._qDocument.contentsChange.connect(self._docChange)
|
||||||
self.selectionChanged.connect(self._updateSelectedStatus)
|
self.selectionChanged.connect(self._updateSelectedStatus)
|
||||||
|
|
||||||
# Document Title
|
# Document Title
|
||||||
@@ -130,9 +134,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.docFooter = GuiDocEditFooter(self)
|
self.docFooter = GuiDocEditFooter(self)
|
||||||
self.docSearch = GuiDocEditSearch(self)
|
self.docSearch = GuiDocEditSearch(self)
|
||||||
|
|
||||||
# Syntax
|
|
||||||
self.highLight = GuiDocHighlighter(qDoc)
|
|
||||||
|
|
||||||
# Context Menu
|
# Context Menu
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.customContextMenuRequested.connect(self._openContextMenu)
|
self.customContextMenuRequested.connect(self._openContextMenu)
|
||||||
@@ -212,7 +213,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
@property
|
@property
|
||||||
def isEmpty(self) -> bool:
|
def isEmpty(self) -> bool:
|
||||||
"""Check if the current document is empty."""
|
"""Check if the current document is empty."""
|
||||||
return self.document().isEmpty()
|
return self._qDocument.isEmpty()
|
||||||
|
|
||||||
##
|
##
|
||||||
# Methods
|
# Methods
|
||||||
@@ -266,7 +267,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.docHeader.matchColours()
|
self.docHeader.matchColours()
|
||||||
self.docFooter.matchColours()
|
self.docFooter.matchColours()
|
||||||
|
|
||||||
self.highLight.initHighlighter()
|
self._qDocument.syntaxHighlighter.initHighlighter()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -312,8 +313,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
# Due to cursor visibility, a part of the margin must be
|
# Due to cursor visibility, a part of the margin must be
|
||||||
# allocated to the document itself. See issue #1112.
|
# allocated to the document itself. See issue #1112.
|
||||||
cW = 2*self.cursorWidth()
|
cW = 2*self.cursorWidth()
|
||||||
qDoc = self.document()
|
self._qDocument.setDocumentMargin(cW)
|
||||||
qDoc.setDocumentMargin(cW)
|
|
||||||
self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
|
self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
|
||||||
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
|
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if CONFIG.showLineEndings:
|
if CONFIG.showLineEndings:
|
||||||
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
|
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
|
||||||
|
|
||||||
qDoc.setDefaultTextOption(theOpt)
|
self._qDocument.setDefaultTextOption(theOpt)
|
||||||
|
|
||||||
# Scroll bars
|
# Scroll bars
|
||||||
if CONFIG.hideVScroll:
|
if CONFIG.hideVScroll:
|
||||||
@@ -377,13 +377,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||||
self._docHandle = tHandle
|
self._docHandle = tHandle
|
||||||
self.highLight.setHandle(tHandle)
|
|
||||||
|
|
||||||
tStart = time()
|
tStart = time()
|
||||||
self._allowAutoReplace(False)
|
self._allowAutoReplace(False)
|
||||||
self.setPlainText(docText)
|
self._qDocument.setTextContent(docText, tHandle)
|
||||||
self._allowAutoReplace(True)
|
self._allowAutoReplace(True)
|
||||||
logger.debug("Document text loaded in %.3f ms", 1000*(time() - tStart))
|
logger.debug("Document text set in %.3f ms", 1000*(time() - tStart))
|
||||||
qApp.processEvents()
|
qApp.processEvents()
|
||||||
|
|
||||||
self._lastEdit = time()
|
self._lastEdit = time()
|
||||||
@@ -404,14 +403,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.docFooter.updateLineCount()
|
self.docFooter.updateLineCount()
|
||||||
|
|
||||||
# This is a hack to fix invisible cursor on an empty document
|
# This is a hack to fix invisible cursor on an empty document
|
||||||
if self.document().characterCount() <= 1:
|
if self._qDocument.characterCount() <= 1:
|
||||||
self.setPlainText("\n")
|
self.setPlainText("\n")
|
||||||
self.setPlainText("")
|
self.setPlainText("")
|
||||||
self.setCursorPosition(0)
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
qApp.processEvents()
|
qApp.processEvents()
|
||||||
self.document().clearUndoRedoStacks()
|
|
||||||
self.setDocumentChanged(False)
|
self.setDocumentChanged(False)
|
||||||
|
self._qDocument.clearUndoRedoStacks()
|
||||||
|
|
||||||
qApp.restoreOverrideCursor()
|
qApp.restoreOverrideCursor()
|
||||||
|
|
||||||
# Update the status bar
|
# Update the status bar
|
||||||
@@ -422,12 +422,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
def updateTagHighLighting(self) -> None:
|
def updateTagHighLighting(self) -> None:
|
||||||
"""Rerun the syntax highlighter on all meta data lines."""
|
"""Rerun the syntax highlighter on all meta data lines."""
|
||||||
self.highLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
||||||
return
|
return
|
||||||
|
|
||||||
def redrawText(self) -> None:
|
def redrawText(self) -> None:
|
||||||
"""Redraw the text by marking the document content as dirty."""
|
"""Redraw the text by marking the document content as dirty."""
|
||||||
self.document().markContentsDirty(0, self.document().characterCount())
|
self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
|
||||||
self.updateDocMargins()
|
self.updateDocMargins()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -561,7 +561,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
paragraph and line separators though.
|
paragraph and line separators though.
|
||||||
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
|
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
|
||||||
"""
|
"""
|
||||||
text = self.document().toRawText()
|
text = self._qDocument.toRawText()
|
||||||
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
|
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
|
||||||
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||||
return text
|
return text
|
||||||
@@ -586,7 +586,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
def setCursorPosition(self, position: int) -> None:
|
def setCursorPosition(self, position: int) -> None:
|
||||||
"""Move the cursor to a given position in the document."""
|
"""Move the cursor to a given position in the document."""
|
||||||
nChars = self.document().characterCount()
|
nChars = self._qDocument.characterCount()
|
||||||
if nChars > 1 and isinstance(position, int):
|
if nChars > 1 and isinstance(position, int):
|
||||||
cursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
cursor.setPosition(minmax(position, 0, nChars-1))
|
cursor.setPosition(minmax(position, 0, nChars-1))
|
||||||
@@ -605,7 +605,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
def setCursorLine(self, line: int | None) -> None:
|
def setCursorLine(self, line: int | None) -> None:
|
||||||
"""Move the cursor to a given line in the document."""
|
"""Move the cursor to a given line in the document."""
|
||||||
if isinstance(line, int) and line > 0:
|
if isinstance(line, int) and line > 0:
|
||||||
block = self.document().findBlockByNumber(line - 1)
|
block = self._qDocument.findBlockByNumber(line - 1)
|
||||||
if block:
|
if block:
|
||||||
self.setCursorPosition(block.position())
|
self.setCursorPosition(block.position())
|
||||||
logger.debug("Cursor moved to line %d", line)
|
logger.debug("Cursor moved to line %d", line)
|
||||||
@@ -638,7 +638,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._spellCheck = state
|
self._spellCheck = state
|
||||||
self.mainGui.mainMenu.setSpellCheck(state)
|
self.mainGui.mainMenu.setSpellCheck(state)
|
||||||
SHARED.project.data.setSpellCheck(state)
|
SHARED.project.data.setSpellCheck(state)
|
||||||
self.highLight.setSpellCheck(state)
|
self._qDocument.syntaxHighlighter.setSpellCheck(state)
|
||||||
if state is False:
|
if state is False:
|
||||||
self.spellCheckDocument()
|
self.spellCheckDocument()
|
||||||
|
|
||||||
@@ -655,7 +655,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
logger.debug("Running spell checker")
|
logger.debug("Running spell checker")
|
||||||
start = time()
|
start = time()
|
||||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||||
self.highLight.rehighlight()
|
self._qDocument.syntaxHighlighter.rehighlight()
|
||||||
qApp.restoreOverrideCursor()
|
qApp.restoreOverrideCursor()
|
||||||
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
|
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
|
||||||
self.statusMessage.emit(self.tr("Spell check complete"))
|
self.statusMessage.emit(self.tr("Spell check complete"))
|
||||||
@@ -994,7 +994,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.wcTimerDoc.start()
|
self.wcTimerDoc.start()
|
||||||
|
|
||||||
if self._doReplace and added == 1:
|
if self._doReplace and added == 1:
|
||||||
self._docAutoReplace(self.document().findBlock(pos))
|
self._docAutoReplace(self._qDocument.findBlock(pos))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1122,7 +1122,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
theWord = cursor.selectedText().strip().strip(self._nonWord)
|
theWord = cursor.selectedText().strip().strip(self._nonWord)
|
||||||
logger.debug("Added '%s' to project dictionary", theWord)
|
logger.debug("Added '%s' to project dictionary", theWord)
|
||||||
SHARED.spelling.addWord(theWord)
|
SHARED.spelling.addWord(theWord)
|
||||||
self.highLight.rehighlightBlock(cursor.block())
|
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@@ -1418,8 +1418,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
posS = theCursor.selectionStart()
|
posS = theCursor.selectionStart()
|
||||||
posE = theCursor.selectionEnd()
|
posE = theCursor.selectionEnd()
|
||||||
|
|
||||||
blockS = self.document().findBlock(posS)
|
blockS = self._qDocument.findBlock(posS)
|
||||||
blockE = self.document().findBlock(posE)
|
blockE = self._qDocument.findBlock(posE)
|
||||||
|
|
||||||
if blockS != blockE:
|
if blockS != blockE:
|
||||||
posE = blockS.position() + blockS.length() - 1
|
posE = blockS.position() + blockS.length() - 1
|
||||||
@@ -1430,14 +1430,14 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
numB = 0
|
numB = 0
|
||||||
for n in range(fLen):
|
for n in range(fLen):
|
||||||
if self.document().characterAt(posS-n-1) == fChar:
|
if self._qDocument.characterAt(posS-n-1) == fChar:
|
||||||
numB += 1
|
numB += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
numA = 0
|
numA = 0
|
||||||
for n in range(fLen):
|
for n in range(fLen):
|
||||||
if self.document().characterAt(posE+n) == fChar:
|
if self._qDocument.characterAt(posE+n) == fChar:
|
||||||
numA += 1
|
numA += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@@ -1487,9 +1487,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
posS = theCursor.selectionStart()
|
posS = theCursor.selectionStart()
|
||||||
posE = theCursor.selectionEnd()
|
posE = theCursor.selectionEnd()
|
||||||
|
|
||||||
qDoc = self.document()
|
blockS = self._qDocument.findBlock(posS)
|
||||||
blockS = qDoc.findBlock(posS)
|
blockE = self._qDocument.findBlock(posE)
|
||||||
blockE = qDoc.findBlock(posE)
|
|
||||||
if blockS != blockE:
|
if blockS != blockE:
|
||||||
posE = blockS.position() + blockS.length() - 1
|
posE = blockS.position() + blockS.length() - 1
|
||||||
|
|
||||||
@@ -1690,16 +1689,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
def _removeInParLineBreaks(self) -> None:
|
def _removeInParLineBreaks(self) -> None:
|
||||||
"""Strip line breaks within paragraphs in the selected text."""
|
"""Strip line breaks within paragraphs in the selected text."""
|
||||||
theCursor = self.textCursor()
|
cursor = self.textCursor()
|
||||||
theDoc = self.document()
|
|
||||||
|
|
||||||
iS = 0
|
iS = 0
|
||||||
iE = theDoc.blockCount() - 1
|
iE = self._qDocument.blockCount() - 1
|
||||||
rS = 0
|
rS = 0
|
||||||
rE = theDoc.characterCount()
|
rE = self._qDocument.characterCount()
|
||||||
if theCursor.hasSelection():
|
if cursor.hasSelection():
|
||||||
sBlock = theDoc.findBlock(theCursor.selectionStart())
|
sBlock = self._qDocument.findBlock(cursor.selectionStart())
|
||||||
eBlock = theDoc.findBlock(theCursor.selectionEnd())
|
eBlock = self._qDocument.findBlock(cursor.selectionEnd())
|
||||||
iS = sBlock.blockNumber()
|
iS = sBlock.blockNumber()
|
||||||
iE = eBlock.blockNumber()
|
iE = eBlock.blockNumber()
|
||||||
rS = sBlock.position()
|
rS = sBlock.position()
|
||||||
@@ -1709,7 +1707,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
currPar = []
|
currPar = []
|
||||||
cleanText = ""
|
cleanText = ""
|
||||||
for i in range(iS, iE+1):
|
for i in range(iS, iE+1):
|
||||||
cBlock = theDoc.findBlockByNumber(i)
|
cBlock = self._qDocument.findBlockByNumber(i)
|
||||||
cText = cBlock.text()
|
cText = cBlock.text()
|
||||||
if cText.strip() == "":
|
if cText.strip() == "":
|
||||||
if currPar:
|
if currPar:
|
||||||
@@ -1726,12 +1724,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
cleanText += " ".join(currPar) + "\n\n"
|
cleanText += " ".join(currPar) + "\n\n"
|
||||||
|
|
||||||
# Replace the text with the cleaned up text
|
# Replace the text with the cleaned up text
|
||||||
theCursor.beginEditBlock()
|
cursor.beginEditBlock()
|
||||||
theCursor.clearSelection()
|
cursor.clearSelection()
|
||||||
theCursor.setPosition(rS)
|
cursor.setPosition(rS)
|
||||||
theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
|
cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
|
||||||
theCursor.insertText(cleanText.rstrip() + "\n")
|
cursor.insertText(cleanText.rstrip() + "\n")
|
||||||
theCursor.endEditBlock()
|
cursor.endEditBlock()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1913,11 +1911,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
# Underscore counts as a part of the word, so check that the
|
# Underscore counts as a part of the word, so check that the
|
||||||
# selection isn't wrapped in italics markers.
|
# selection isn't wrapped in italics markers.
|
||||||
reSelect = False
|
reSelect = False
|
||||||
qDoc = self.document()
|
if self._qDocument.characterAt(posS) == "_":
|
||||||
if qDoc.characterAt(posS) == "_":
|
|
||||||
posS += 1
|
posS += 1
|
||||||
reSelect = True
|
reSelect = True
|
||||||
if qDoc.characterAt(posE) == "_":
|
if self._qDocument.characterAt(posE) == "_":
|
||||||
posE -= 1
|
posE -= 1
|
||||||
reSelect = True
|
reSelect = True
|
||||||
if reSelect:
|
if reSelect:
|
||||||
@@ -2838,7 +2835,7 @@ class GuiDocEditFooter(QWidget):
|
|||||||
else:
|
else:
|
||||||
theCursor = self.docEditor.textCursor()
|
theCursor = self.docEditor.textCursor()
|
||||||
iLine = theCursor.blockNumber() + 1
|
iLine = theCursor.blockNumber() + 1
|
||||||
iDist = 100*iLine/self.docEditor.document().blockCount()
|
iDist = 100*iLine/self.docEditor._qDocument.blockCount()
|
||||||
self.linesText.setText(
|
self.linesText.setText(
|
||||||
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
|
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
|
||||||
)
|
)
|
||||||
@@ -2869,7 +2866,7 @@ class GuiDocEditFooter(QWidget):
|
|||||||
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
|
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
|
||||||
)
|
)
|
||||||
|
|
||||||
byteSize = self.docEditor.document().characterCount()
|
byteSize = self.docEditor._qDocument.characterCount()
|
||||||
self.wordsText.setToolTip(
|
self.wordsText.setToolTip(
|
||||||
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
|
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
logger.debug("Create: GuiDocHighlighter")
|
logger.debug("Create: GuiDocHighlighter")
|
||||||
|
|
||||||
|
self._tItem = None
|
||||||
self._tHandle = None
|
self._tHandle = None
|
||||||
self._spellCheck = False
|
self._spellCheck = False
|
||||||
self._spellRx = QRegularExpression()
|
self._spellRx = QRegularExpression()
|
||||||
@@ -79,11 +80,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@property
|
|
||||||
def spellCheck(self) -> bool:
|
|
||||||
"""Check if spell checking is enabled."""
|
|
||||||
return self._spellCheck
|
|
||||||
|
|
||||||
def initHighlighter(self) -> None:
|
def initHighlighter(self) -> None:
|
||||||
"""Initialise the syntax highlighter, setting all the colour
|
"""Initialise the syntax highlighter, setting all the colour
|
||||||
rules and building the RegExes.
|
rules and building the RegExes.
|
||||||
@@ -248,6 +244,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
def setHandle(self, tHandle: str) -> None:
|
def setHandle(self, tHandle: str) -> None:
|
||||||
"""Set the handle of the currently highlighted document."""
|
"""Set the handle of the currently highlighted document."""
|
||||||
self._tHandle = tHandle
|
self._tHandle = tHandle
|
||||||
|
self._tItem = SHARED.project.tree[tHandle]
|
||||||
|
logger.debug(
|
||||||
|
"Syntax highlighter %s for item '%s'",
|
||||||
|
"enabled" if self._tItem else "disabled", tHandle
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -284,27 +285,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
if text.startswith("@"): # Keywords and commands
|
if text.startswith("@"): # Keywords and commands
|
||||||
self.setCurrentBlockState(self.BLOCK_META)
|
self.setCurrentBlockState(self.BLOCK_META)
|
||||||
pIndex = SHARED.project.index
|
if self._tItem:
|
||||||
tItem = SHARED.project.tree[self._tHandle]
|
pIndex = SHARED.project.index
|
||||||
if tItem is None:
|
isValid, theBits, thePos = pIndex.scanThis(text)
|
||||||
return
|
isGood = pIndex.checkThese(theBits, self._tItem)
|
||||||
|
if isValid:
|
||||||
isValid, theBits, thePos = pIndex.scanThis(text)
|
for n, theBit in enumerate(theBits):
|
||||||
isGood = pIndex.checkThese(theBits, tItem)
|
xPos = thePos[n]
|
||||||
if isValid:
|
xLen = len(theBit)
|
||||||
for n, theBit in enumerate(theBits):
|
if isGood[n]:
|
||||||
xPos = thePos[n]
|
if n == 0:
|
||||||
xLen = len(theBit)
|
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||||
if isGood[n]:
|
else:
|
||||||
if n == 0:
|
self.setFormat(xPos, xLen, self._hStyles["value"])
|
||||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
|
||||||
else:
|
else:
|
||||||
self.setFormat(xPos, xLen, self._hStyles["value"])
|
kwFmt = self.format(xPos)
|
||||||
else:
|
kwFmt.setUnderlineColor(self._colError)
|
||||||
kwFmt = self.format(xPos)
|
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||||
kwFmt.setUnderlineColor(self._colError)
|
self.setFormat(xPos, xLen, kwFmt)
|
||||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
|
||||||
self.setFormat(xPos, xLen, kwFmt)
|
|
||||||
|
|
||||||
# We never want to run the spell checker on keyword/values,
|
# We never want to run the spell checker on keyword/values,
|
||||||
# so we force a return here
|
# so we force a return here
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
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 PyQt5.QtCore import QObject
|
||||||
|
|
||||||
|
from PyQt5.QtGui import QTextDocument
|
||||||
|
from PyQt5.QtWidgets import QPlainTextDocumentLayout
|
||||||
|
|
||||||
|
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def syntaxHighlighter(self) -> GuiDocHighlighter:
|
||||||
|
return self._syntax
|
||||||
|
|
||||||
|
def setTextContent(self, text: str, tHandle: str) -> None:
|
||||||
|
"""Set the text content of the document."""
|
||||||
|
self._syntax.setHandle(tHandle)
|
||||||
|
self.setPlainText(text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class GuiTextDocument
|
||||||
@@ -621,10 +621,10 @@ class GuiMain(QMainWindow):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if nHandle is not None:
|
if nHandle is not None:
|
||||||
self.openDocument(nHandle, tLine=0, doScroll=True)
|
self.openDocument(nHandle, tLine=1, doScroll=True)
|
||||||
return True
|
return True
|
||||||
elif wrapAround:
|
elif wrapAround:
|
||||||
self.openDocument(fHandle, tLine=0, doScroll=True)
|
self.openDocument(fHandle, tLine=1, doScroll=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
%%~name: Making a Scene
|
%%~name: Making a Scene
|
||||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||||
%%~kind: NOVEL/DOCUMENT
|
%%~kind: NOVEL/DOCUMENT
|
||||||
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
|
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
|
||||||
%%~date: Unknown/2023-08-25 16:56:11
|
%%~date: Unknown/2023-09-07 19:00:10
|
||||||
### Making a Scene
|
### Making a Scene
|
||||||
|
|
||||||
@pov: Jane
|
@pov: Jane
|
||||||
@@ -15,7 +15,7 @@ Each paragraph in the scene is separated by a blank line. The text supports mini
|
|||||||
|
|
||||||
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
|
In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
|
||||||
|
|
||||||
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings.
|
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
|
||||||
|
|
||||||
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.
|
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-01 20:48:55">
|
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-07 19:14:50">
|
||||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1522" autoCount="237" editTime="75353">
|
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1531" autoCount="238" editTime="75619">
|
||||||
<name>Sample Project</name>
|
<name>Sample Project</name>
|
||||||
<title>Sample Project</title>
|
<title>Sample Project</title>
|
||||||
<author>Jane Smith</author>
|
<author>Jane Smith</author>
|
||||||
@@ -58,27 +58,27 @@
|
|||||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="66" />
|
<meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="465" />
|
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="531" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="310" />
|
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="0" />
|
||||||
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
|
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
|
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="0" />
|
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="1940" />
|
||||||
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
|
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="188" />
|
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="356" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="0" />
|
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="237" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
|
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||||
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="104" />
|
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="387" />
|
||||||
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
|
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
|
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
|
||||||
@@ -102,11 +102,11 @@
|
|||||||
<name status="sf12341" import="ia857f0">Main Characters</name>
|
<name status="sf12341" import="ia857f0">Main Characters</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="24" />
|
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="65" />
|
||||||
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
|
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
|
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="25" />
|
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="71" />
|
||||||
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
|
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
|
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
|
||||||
@@ -114,15 +114,15 @@
|
|||||||
<name status="sf12341" import="ia857f0">Locations</name>
|
<name status="sf12341" import="ia857f0">Locations</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="20" />
|
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="111" />
|
||||||
<name status="sf12341" import="i56be10" active="yes">Earth</name>
|
<name status="sf12341" import="i56be10" active="yes">Earth</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="133" />
|
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="135" />
|
||||||
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
|
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
|
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
|
||||||
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="45" />
|
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="62" />
|
||||||
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
|
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
|
||||||
</item>
|
</item>
|
||||||
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
|
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
|
||||||
|
|||||||
Reference in New Issue
Block a user