Test Coverage of the Editor (#787)
* Drop the _qDocument class variables * Split up the current doceditor test file * Add tests for editor init, load and save * Add tests for editor meta functions and actions * Add tests for editor insert functions * Add tests for editor text manipulation and block formatting * Fix code style * Add text coverage for alignment and indent
This commit is contained in:
committed by
GitHub
parent
1d16cc2a03
commit
36be7b0b33
+349
-331
@@ -111,9 +111,9 @@ class GuiDocEditor(QTextEdit):
|
||||
self._typPadChar = " "
|
||||
|
||||
# Core Elements and Signals
|
||||
self._qDocument = self.document()
|
||||
self._qDocument.contentsChange.connect(self._docChange)
|
||||
self._qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged)
|
||||
qDoc = self.document()
|
||||
qDoc.contentsChange.connect(self._docChange)
|
||||
qDoc.documentLayout().documentSizeChanged.connect(self._docSizeChanged)
|
||||
|
||||
# Document Title
|
||||
self.docHeader = GuiDocEditHeader(self)
|
||||
@@ -121,7 +121,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self.docSearch = GuiDocEditSearch(self)
|
||||
|
||||
# Syntax
|
||||
self.hLight = GuiDocHighlighter(self._qDocument, self.theParent)
|
||||
self.hLight = GuiDocHighlighter(qDoc, self.theParent)
|
||||
|
||||
# Context Menu
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
@@ -221,9 +221,10 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
# Set font
|
||||
theFont = QFont()
|
||||
qDoc = self.document()
|
||||
if self.mainConf.textFont is None:
|
||||
# If none is defined, set the default back to config
|
||||
self.mainConf.textFont = self._qDocument.defaultFont().family()
|
||||
self.mainConf.textFont = qDoc.defaultFont().family()
|
||||
|
||||
theFont.setFamily(self.mainConf.textFont)
|
||||
theFont.setPointSize(self.mainConf.textSize)
|
||||
@@ -246,7 +247,7 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
# Set default text margins
|
||||
cM = self.mainConf.getTextMargin()
|
||||
self._qDocument.setDocumentMargin(0)
|
||||
qDoc.setDocumentMargin(0)
|
||||
self.setViewportMargins(cM, cM, cM, cM)
|
||||
|
||||
# Also set the document text options for the document text flow
|
||||
@@ -259,7 +260,7 @@ class GuiDocEditor(QTextEdit):
|
||||
if self.mainConf.showLineEndings:
|
||||
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
|
||||
|
||||
self._qDocument.setDefaultTextOption(theOpt)
|
||||
qDoc.setDefaultTextOption(theOpt)
|
||||
|
||||
# Scroll bars
|
||||
if self.mainConf.hideVScroll:
|
||||
@@ -275,7 +276,7 @@ class GuiDocEditor(QTextEdit):
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
|
||||
# Initialise the syntax highlighter
|
||||
@@ -370,7 +371,6 @@ class GuiDocEditor(QTextEdit):
|
||||
self.setCursorLine(tLine)
|
||||
|
||||
self.docFooter.updateLineCount()
|
||||
self.lengthLast = self._qDocument.characterCount()
|
||||
self._docHeaders = self.theIndex.getHandleHeaders(self._docHandle)
|
||||
|
||||
qApp.processEvents()
|
||||
@@ -378,7 +378,7 @@ class GuiDocEditor(QTextEdit):
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
# This is a hack to fix invisble cursor on an empty document
|
||||
if self._qDocument.characterCount() <= 1:
|
||||
if self.document().characterCount() <= 1:
|
||||
self.setPlainText("\n")
|
||||
self.setPlainText("")
|
||||
self.setCursorPosition(0)
|
||||
@@ -391,7 +391,7 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
return True
|
||||
|
||||
def updateTagHighLighting(self, forceBigDoc=False):
|
||||
def updateTagHighLighting(self):
|
||||
"""Rerun the syntax highlighter on all meta data lines.
|
||||
"""
|
||||
self.hLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
|
||||
@@ -400,7 +400,7 @@ class GuiDocEditor(QTextEdit):
|
||||
def redrawText(self):
|
||||
"""Redraw the text by marking the document content as "dirty".
|
||||
"""
|
||||
self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
|
||||
self.document().markContentsDirty(0, self.document().characterCount())
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
@@ -532,9 +532,9 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
tmpDocChanged = self._docChanged
|
||||
if self.mainConf.scrollPastEnd:
|
||||
docFrame = self._qDocument.rootFrame().frameFormat()
|
||||
docFrame = self.document().rootFrame().frameFormat()
|
||||
docFrame.setBottomMargin(max(0, 0.9*(wH - uM - lM - 4*tB)))
|
||||
self._qDocument.rootFrame().setFrameFormat(docFrame)
|
||||
self.document().rootFrame().setFrameFormat(docFrame)
|
||||
|
||||
# This is needed as the setFrameFormat function itself will
|
||||
# trigger the contetsChanged signal which sets _docChanged, so we
|
||||
@@ -576,7 +576,7 @@ class GuiDocEditor(QTextEdit):
|
||||
def isEmpty(self):
|
||||
"""Wrapper function to check if the current document is empty.
|
||||
"""
|
||||
return self._qDocument.isEmpty()
|
||||
return self.document().isEmpty()
|
||||
|
||||
def currentDictionary(self):
|
||||
"""Return the current dictionary object.
|
||||
@@ -596,7 +596,7 @@ class GuiDocEditor(QTextEdit):
|
||||
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
|
||||
"""
|
||||
if self.mainConf.verQtValue >= 50900:
|
||||
theText = self._qDocument.toRawText()
|
||||
theText = self.document().toRawText()
|
||||
theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
|
||||
theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||
else:
|
||||
@@ -628,7 +628,7 @@ class GuiDocEditor(QTextEdit):
|
||||
if not isinstance(thePosition, int):
|
||||
return False
|
||||
|
||||
nChars = self._qDocument.characterCount()
|
||||
nChars = self.document().characterCount()
|
||||
if nChars > 1:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(min(max(thePosition, 0), nChars-1))
|
||||
@@ -652,7 +652,7 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
|
||||
if theLine >= 0:
|
||||
theBlock = self._qDocument.findBlockByLineNumber(theLine)
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
self.docFooter.updateLineCount()
|
||||
@@ -710,7 +710,8 @@ class GuiDocEditor(QTextEdit):
|
||||
def spellCheckDocument(self):
|
||||
"""Rerun the highlighter to update spell checking status of the
|
||||
currently loaded text. The fastest way to do this, at least as
|
||||
of Qt 5.13, is to clear the text and put it back.
|
||||
of Qt 5.13, is to clear the text and put it back. This clears
|
||||
the undo stack, so we only do it for big documents.
|
||||
"""
|
||||
logger.verbose("Running spell checker")
|
||||
if self._spellCheck:
|
||||
@@ -739,11 +740,16 @@ class GuiDocEditor(QTextEdit):
|
||||
passed to it without having to consider the internal logic of
|
||||
this class when calling these actions from other classes.
|
||||
"""
|
||||
logger.verbose("Requesting action: '%s'", theAction.name)
|
||||
if self._docHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
|
||||
if not isinstance(theAction, nwDocAction):
|
||||
logger.error("Not a document action")
|
||||
return False
|
||||
|
||||
logger.verbose("Requesting action: %s" % theAction.name)
|
||||
|
||||
self._allowAutoReplace(False)
|
||||
if theAction == nwDocAction.UNDO:
|
||||
self.undo()
|
||||
@@ -1012,7 +1018,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self._lastEdit = time()
|
||||
self._lastFind = None
|
||||
|
||||
if self._qDocument.characterCount() > nwConst.MAX_DOCSIZE:
|
||||
if self.document().characterCount() > nwConst.MAX_DOCSIZE:
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"The document has grown too big and you cannot add more text to it. "
|
||||
"The maximum size of a single novelWriter document is {0} MB."
|
||||
@@ -1029,7 +1035,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self.wcTimer.start()
|
||||
|
||||
if self._doReplace and chrAdd == 1:
|
||||
self._docAutoReplace(self._qDocument.findBlock(thePos))
|
||||
self._docAutoReplace(self.document().findBlock(thePos))
|
||||
|
||||
return
|
||||
|
||||
@@ -1198,7 +1204,7 @@ class GuiDocEditor(QTextEdit):
|
||||
# Must not be emitted if docHandle is None!
|
||||
self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
|
||||
|
||||
self._checkDocSize(self._qDocument.characterCount())
|
||||
self._checkDocSize(self.document().characterCount())
|
||||
self.docFooter.updateCounts()
|
||||
|
||||
return
|
||||
@@ -1211,7 +1217,7 @@ class GuiDocEditor(QTextEdit):
|
||||
moved to has been drawn before the move is made.
|
||||
"""
|
||||
if self._queuePos is not None:
|
||||
thePos = self._qDocument.documentLayout().hitTest(
|
||||
thePos = self.document().documentLayout().hitTest(
|
||||
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
|
||||
)
|
||||
if self._queuePos <= thePos:
|
||||
@@ -1362,226 +1368,74 @@ class GuiDocEditor(QTextEdit):
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
# Internal Functions : Text Manipulation
|
||||
##
|
||||
|
||||
def _followTag(self, theCursor=None, loadTag=True):
|
||||
"""Activated by Ctrl+Enter. Checks that we're in a block
|
||||
starting with '@'. We then find the word under the cursor and
|
||||
check that it is after the ':'. If all this is fine, we have a
|
||||
tag and can tell the document viewer to try and find and load
|
||||
the file where the tag is defined.
|
||||
def _toggleFormat(self, fLen, fChar):
|
||||
"""Toggle the formatting of a specific type for a piece of text.
|
||||
If more than one block is selected, the formatting is applied to
|
||||
the first block.
|
||||
"""
|
||||
if theCursor is None:
|
||||
theCursor = self.textCursor()
|
||||
|
||||
theBlock = theCursor.block()
|
||||
theText = theBlock.text()
|
||||
|
||||
if len(theText) == 0:
|
||||
theCursor = self._autoSelect()
|
||||
if not theCursor.hasSelection():
|
||||
logger.warning("No selection made, nothing to do")
|
||||
return False
|
||||
|
||||
if theText.startswith("@"):
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
theWord = theCursor.selectedText()
|
||||
cPos = theText.find(":")
|
||||
wPos = theCursor.selectionStart() - theBlock.position()
|
||||
if wPos <= cPos:
|
||||
return False
|
||||
blockS = self.document().findBlock(posS)
|
||||
blockE = self.document().findBlock(posE)
|
||||
|
||||
if loadTag:
|
||||
logger.verbose("Attempting to follow tag '%s'", theWord)
|
||||
self.theParent.docViewer.loadFromTag(theWord)
|
||||
if blockS != blockE:
|
||||
posE = blockS.position() + blockS.length() - 1
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posS, QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posE, QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
numB = 0
|
||||
for n in range(fLen):
|
||||
if self.document().characterAt(posS-n-1) == fChar:
|
||||
numB += 1
|
||||
else:
|
||||
logger.verbose("Potential tag '%s'", theWord)
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _openSpellContext(self):
|
||||
"""Opens the spell check context menu at the current point of
|
||||
the cursor.
|
||||
"""
|
||||
self._openContextMenu(self.cursorRect().center())
|
||||
return
|
||||
|
||||
def _docAutoReplace(self, theBlock):
|
||||
"""Auto-replace text elements based on main configuration.
|
||||
"""
|
||||
if not theBlock.isValid():
|
||||
return
|
||||
|
||||
theText = theBlock.text()
|
||||
theCursor = self.textCursor()
|
||||
thePos = theCursor.positionInBlock()
|
||||
theLen = len(theText)
|
||||
|
||||
if theLen < 1 or thePos-1 > theLen:
|
||||
return
|
||||
|
||||
theOne = theText[thePos-1:thePos]
|
||||
theTwo = theText[thePos-2:thePos]
|
||||
theThree = theText[thePos-3:thePos]
|
||||
|
||||
if not theOne: # Makes Neo sad
|
||||
return
|
||||
|
||||
nDelete = 0
|
||||
tInsert = theOne
|
||||
|
||||
if self.mainConf.doReplaceDQuote and theTwo == ' "':
|
||||
nDelete = 1
|
||||
tInsert = self._typDQOpen
|
||||
|
||||
elif self.mainConf.doReplaceDQuote and theOne == '"':
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
tInsert = self._typDQOpen
|
||||
numA = 0
|
||||
for n in range(fLen):
|
||||
if self.document().characterAt(posE+n) == fChar:
|
||||
numA += 1
|
||||
else:
|
||||
tInsert = self._typDQClose
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theTwo == " '":
|
||||
nDelete = 1
|
||||
tInsert = self._typSQOpen
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theOne == "'":
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
tInsert = self._typSQOpen
|
||||
else:
|
||||
tInsert = self._typSQClose
|
||||
|
||||
elif self.mainConf.doReplaceDash and theThree == "---":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "--":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_ENDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH + "-":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDots and theThree == "...":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_HELLIP
|
||||
|
||||
tCheck = tInsert
|
||||
if tCheck in self.mainConf.fmtPadBefore:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = self._typPadChar + tInsert
|
||||
|
||||
if tCheck in self.mainConf.fmtPadAfter:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = tInsert + self._typPadChar
|
||||
|
||||
if nDelete > 0:
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
|
||||
theCursor.insertText(tInsert)
|
||||
|
||||
return
|
||||
|
||||
def _updateHeaders(self, checkPos=False, checkLevel=False):
|
||||
"""Update the headers record and return True if anything
|
||||
changed, if a check flag was provided.
|
||||
"""
|
||||
if self._docHandle is None:
|
||||
return False
|
||||
|
||||
newHeaders = self.theIndex.getHandleHeaders(self._docHandle)
|
||||
if checkPos:
|
||||
newPos = [x[0] for x in newHeaders]
|
||||
oldPos = [x[0] for x in self._docHeaders]
|
||||
if checkLevel:
|
||||
newLev = [x[1] for x in newHeaders]
|
||||
oldLev = [x[1] for x in self._docHeaders]
|
||||
|
||||
self._docHeaders = newHeaders
|
||||
|
||||
if checkPos:
|
||||
return newPos != oldPos
|
||||
if checkLevel:
|
||||
return newLev != oldLev
|
||||
|
||||
return False
|
||||
|
||||
def _replaceQuotes(self, sQuote, oQuote, cQuote):
|
||||
"""Replace all straight quotes in the selected text.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
closeCheck = (
|
||||
" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
|
||||
)
|
||||
|
||||
self._allowAutoReplace(False)
|
||||
for posC in range(posS, posE+1):
|
||||
theCursor.setPosition(posC)
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
selText = theCursor.selectedText()
|
||||
|
||||
nS = len(selText)
|
||||
if nS == 2:
|
||||
pC = selText[0]
|
||||
cC = selText[1]
|
||||
elif nS == 1:
|
||||
pC = " "
|
||||
cC = selText[0]
|
||||
else:
|
||||
continue
|
||||
|
||||
if cC != sQuote:
|
||||
continue
|
||||
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posC)
|
||||
if pC in closeCheck:
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(oQuote)
|
||||
theCursor.endEditBlock()
|
||||
else:
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(cQuote)
|
||||
theCursor.endEditBlock()
|
||||
|
||||
self._allowAutoReplace(True)
|
||||
break
|
||||
|
||||
if fLen == min(numA, numB):
|
||||
self._clearSurrounding(theCursor, fLen)
|
||||
else:
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Please select some text before calling replace quotes."
|
||||
), nwAlert.ERROR)
|
||||
self._wrapSelection(fChar*fLen)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _checkDocSize(self, theSize):
|
||||
"""Check if document size crosses the big document limit set in
|
||||
config. If so, we will set the big document flag to True.
|
||||
def _clearSurrounding(self, theCursor, nChars):
|
||||
"""Clears n characters before and after the cursor.
|
||||
"""
|
||||
bigLim = self.mainConf.bigDocLimit*1000
|
||||
newState = theSize > bigLim
|
||||
if not theCursor.hasSelection():
|
||||
logger.warning("No selection made, nothing to do")
|
||||
return False
|
||||
|
||||
if newState != self._bigDoc:
|
||||
if newState:
|
||||
logger.info(
|
||||
f"The document size is {theSize:n} > {bigLim:n}, "
|
||||
f"big doc mode has been enabled"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"The document size is {theSize:n} <= {bigLim:n}, "
|
||||
f"big doc mode has been disabled"
|
||||
)
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
theCursor.clearSelection()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.setPosition(posS)
|
||||
for i in range(nChars):
|
||||
theCursor.deletePreviousChar()
|
||||
theCursor.setPosition(posE)
|
||||
for i in range(nChars):
|
||||
theCursor.deletePreviousChar()
|
||||
theCursor.endEditBlock()
|
||||
theCursor.clearSelection()
|
||||
|
||||
self._bigDoc = newState
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _wrapSelection(self, tBefore, tAfter=None):
|
||||
"""Wraps the selected text in whatever is in tBefore and tAfter.
|
||||
@@ -1593,120 +1447,84 @@ class GuiDocEditor(QTextEdit):
|
||||
tAfter = tBefore
|
||||
|
||||
theCursor = self._autoSelect()
|
||||
if theCursor.hasSelection():
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
|
||||
blockS = self._qDocument.findBlock(posS)
|
||||
blockE = self._qDocument.findBlock(posE)
|
||||
if blockS != blockE:
|
||||
posE = blockS.position() + blockS.length() - 1
|
||||
|
||||
theCursor.clearSelection()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.setPosition(posE)
|
||||
theCursor.insertText(tAfter)
|
||||
theCursor.setPosition(posS)
|
||||
theCursor.insertText(tBefore)
|
||||
theCursor.endEditBlock()
|
||||
|
||||
theCursor.setPosition(posE + len(tBefore), QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posS + len(tBefore), QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
else:
|
||||
if not theCursor.hasSelection():
|
||||
logger.warning("No selection made, nothing to do")
|
||||
return
|
||||
return False
|
||||
|
||||
def _clearSurrounding(self, theCursor, nChars):
|
||||
"""Clears n characters before and after the cursor.
|
||||
"""
|
||||
if theCursor.hasSelection():
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
theCursor.clearSelection()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.setPosition(posS)
|
||||
for i in range(nChars):
|
||||
theCursor.deletePreviousChar()
|
||||
theCursor.setPosition(posE)
|
||||
for i in range(nChars):
|
||||
theCursor.deletePreviousChar()
|
||||
theCursor.endEditBlock()
|
||||
theCursor.clearSelection()
|
||||
else:
|
||||
logger.warning("No selection made, nothing to do")
|
||||
return
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
|
||||
def _autoSelect(self):
|
||||
"""Returns a cursor which may or may not have a selection based
|
||||
on user settings and document action.
|
||||
qDoc = self.document()
|
||||
blockS = qDoc.findBlock(posS)
|
||||
blockE = qDoc.findBlock(posE)
|
||||
if blockS != blockE:
|
||||
posE = blockS.position() + blockS.length() - 1
|
||||
|
||||
theCursor.clearSelection()
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.setPosition(posE)
|
||||
theCursor.insertText(tAfter)
|
||||
theCursor.setPosition(posS)
|
||||
theCursor.insertText(tBefore)
|
||||
theCursor.endEditBlock()
|
||||
|
||||
theCursor.setPosition(posE + len(tBefore), QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posS + len(tBefore), QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
return True
|
||||
|
||||
def _replaceQuotes(self, sQuote, oQuote, cQuote):
|
||||
"""Replace all straight quotes in the selected text.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if self.mainConf.autoSelect and not theCursor.hasSelection():
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
if not theCursor.hasSelection():
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Please select some text before calling replace quotes."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# Underscore counts as a part of the word, so check that the
|
||||
# selection isn't wrapped in italics markers.
|
||||
reSelect = False
|
||||
if self._qDocument.characterAt(posS) == "_":
|
||||
posS += 1
|
||||
reSelect = True
|
||||
if self._qDocument.characterAt(posE) == "_":
|
||||
posE -= 1
|
||||
reSelect = True
|
||||
if reSelect:
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posS, QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posE-1, QTextCursor.KeepAnchor)
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
closeCheck = (
|
||||
" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
|
||||
)
|
||||
|
||||
self.setTextCursor(theCursor)
|
||||
self._allowAutoReplace(False)
|
||||
for posC in range(posS, posE+1):
|
||||
theCursor.setPosition(posC)
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
selText = theCursor.selectedText()
|
||||
|
||||
return theCursor
|
||||
nS = len(selText)
|
||||
if nS == 2:
|
||||
pC = selText[0]
|
||||
cC = selText[1]
|
||||
elif nS == 1:
|
||||
pC = " "
|
||||
cC = selText[0]
|
||||
else: # pragma: no cover
|
||||
continue
|
||||
|
||||
def _toggleFormat(self, fLen, fChar):
|
||||
"""Toggle the formatting of a specific type for a piece of text.
|
||||
If more than one block is selected, the formatting is applied to
|
||||
the first block.
|
||||
"""
|
||||
theCursor = self._autoSelect()
|
||||
if theCursor.hasSelection():
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
if cC != sQuote:
|
||||
continue
|
||||
|
||||
blockS = self._qDocument.findBlock(posS)
|
||||
blockE = self._qDocument.findBlock(posE)
|
||||
|
||||
if blockS != blockE:
|
||||
posE = blockS.position() + blockS.length() - 1
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posS, QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posE, QTextCursor.KeepAnchor)
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
numB = 0
|
||||
for n in range(fLen):
|
||||
if self._qDocument.characterAt(posS-n-1) == fChar:
|
||||
numB += 1
|
||||
else:
|
||||
break
|
||||
|
||||
numA = 0
|
||||
for n in range(fLen):
|
||||
if self._qDocument.characterAt(posE+n) == fChar:
|
||||
numA += 1
|
||||
else:
|
||||
break
|
||||
|
||||
cLevel = min(numB, numA)
|
||||
if cLevel == fLen:
|
||||
self._clearSurrounding(theCursor, fLen)
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posC)
|
||||
if pC in closeCheck:
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(oQuote)
|
||||
theCursor.endEditBlock()
|
||||
else:
|
||||
self._wrapSelection(fChar*fLen)
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(cQuote)
|
||||
theCursor.endEditBlock()
|
||||
|
||||
return
|
||||
self._allowAutoReplace(True)
|
||||
|
||||
return True
|
||||
|
||||
def _formatBlock(self, docAction):
|
||||
"""Changes the block format of the block under the cursor.
|
||||
@@ -1874,6 +1692,206 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _followTag(self, theCursor=None, loadTag=True):
|
||||
"""Activated by Ctrl+Enter. Checks that we're in a block
|
||||
starting with '@'. We then find the word under the cursor and
|
||||
check that it is after the ':'. If all this is fine, we have a
|
||||
tag and can tell the document viewer to try and find and load
|
||||
the file where the tag is defined.
|
||||
"""
|
||||
if theCursor is None:
|
||||
theCursor = self.textCursor()
|
||||
|
||||
theBlock = theCursor.block()
|
||||
theText = theBlock.text()
|
||||
|
||||
if len(theText) == 0:
|
||||
return False
|
||||
|
||||
if theText.startswith("@"):
|
||||
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
theWord = theCursor.selectedText()
|
||||
cPos = theText.find(":")
|
||||
wPos = theCursor.selectionStart() - theBlock.position()
|
||||
if wPos <= cPos:
|
||||
return False
|
||||
|
||||
if loadTag:
|
||||
logger.verbose("Attempting to follow tag '%s'" % theWord)
|
||||
self.theParent.docViewer.loadFromTag(theWord)
|
||||
else:
|
||||
logger.verbose("Potential tag '%s'" % theWord)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _openSpellContext(self):
|
||||
"""Opens the spell check context menu at the current point of
|
||||
the cursor.
|
||||
"""
|
||||
self._openContextMenu(self.cursorRect().center())
|
||||
return
|
||||
|
||||
def _docAutoReplace(self, theBlock):
|
||||
"""Auto-replace text elements based on main configuration.
|
||||
"""
|
||||
if not theBlock.isValid():
|
||||
return
|
||||
|
||||
theText = theBlock.text()
|
||||
theCursor = self.textCursor()
|
||||
thePos = theCursor.positionInBlock()
|
||||
theLen = len(theText)
|
||||
|
||||
if theLen < 1 or thePos-1 > theLen:
|
||||
return
|
||||
|
||||
theOne = theText[thePos-1:thePos]
|
||||
theTwo = theText[thePos-2:thePos]
|
||||
theThree = theText[thePos-3:thePos]
|
||||
|
||||
if not theOne:
|
||||
# Sorry, Neo and Zathras
|
||||
return
|
||||
|
||||
nDelete = 0
|
||||
tInsert = theOne
|
||||
|
||||
if self.mainConf.doReplaceDQuote and theTwo == ' "':
|
||||
nDelete = 1
|
||||
tInsert = self._typDQOpen
|
||||
|
||||
elif self.mainConf.doReplaceDQuote and theOne == '"':
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
tInsert = self._typDQOpen
|
||||
else:
|
||||
tInsert = self._typDQClose
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theTwo == " '":
|
||||
nDelete = 1
|
||||
tInsert = self._typSQOpen
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theOne == "'":
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
tInsert = self._typSQOpen
|
||||
else:
|
||||
tInsert = self._typSQClose
|
||||
|
||||
elif self.mainConf.doReplaceDash and theThree == "---":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "--":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_ENDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH + "-":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDots and theThree == "...":
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_HELLIP
|
||||
|
||||
tCheck = tInsert
|
||||
if tCheck in self.mainConf.fmtPadBefore:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = self._typPadChar + tInsert
|
||||
|
||||
if tCheck in self.mainConf.fmtPadAfter:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = tInsert + self._typPadChar
|
||||
|
||||
if nDelete > 0:
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
|
||||
theCursor.insertText(tInsert)
|
||||
|
||||
return
|
||||
|
||||
def _updateHeaders(self, checkPos=False, checkLevel=False):
|
||||
"""Update the headers record and return True if anything
|
||||
changed, if a check flag was provided.
|
||||
"""
|
||||
if self._docHandle is None:
|
||||
return False
|
||||
|
||||
newHeaders = self.theIndex.getHandleHeaders(self._docHandle)
|
||||
if checkPos:
|
||||
newPos = [x[0] for x in newHeaders]
|
||||
oldPos = [x[0] for x in self._docHeaders]
|
||||
if checkLevel:
|
||||
newLev = [x[1] for x in newHeaders]
|
||||
oldLev = [x[1] for x in self._docHeaders]
|
||||
|
||||
self._docHeaders = newHeaders
|
||||
|
||||
if checkPos:
|
||||
return newPos != oldPos
|
||||
if checkLevel:
|
||||
return newLev != oldLev
|
||||
|
||||
return False
|
||||
|
||||
def _checkDocSize(self, theSize):
|
||||
"""Check if document size crosses the big document limit set in
|
||||
config. If so, we will set the big document flag to True.
|
||||
"""
|
||||
bigLim = round(self.mainConf.bigDocLimit*1000)
|
||||
newState = theSize > bigLim
|
||||
|
||||
if newState != self._bigDoc:
|
||||
if newState:
|
||||
logger.info(
|
||||
f"The document size is {theSize:n} > {bigLim:n}, "
|
||||
f"big doc mode has been enabled"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"The document size is {theSize:n} <= {bigLim:n}, "
|
||||
f"big doc mode has been disabled"
|
||||
)
|
||||
|
||||
self._bigDoc = newState
|
||||
|
||||
return
|
||||
|
||||
def _autoSelect(self):
|
||||
"""Returns a cursor which may or may not have a selection based
|
||||
on user settings and document action.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if self.mainConf.autoSelect and not theCursor.hasSelection():
|
||||
theCursor.select(QTextCursor.WordUnderCursor)
|
||||
posS = theCursor.selectionStart()
|
||||
posE = theCursor.selectionEnd()
|
||||
|
||||
# Underscore counts as a part of the word, so check that the
|
||||
# selection isn't wrapped in italics markers.
|
||||
reSelect = False
|
||||
qDoc = self.document()
|
||||
if qDoc.characterAt(posS) == "_":
|
||||
posS += 1
|
||||
reSelect = True
|
||||
if qDoc.characterAt(posE) == "_":
|
||||
posE -= 1
|
||||
reSelect = True
|
||||
if reSelect:
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(posS, QTextCursor.MoveAnchor)
|
||||
theCursor.setPosition(posE-1, QTextCursor.KeepAnchor)
|
||||
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
return theCursor
|
||||
|
||||
def _makeSelection(self, selMode):
|
||||
"""Wrapper function to select text based on a selection mode.
|
||||
"""
|
||||
@@ -2753,7 +2771,7 @@ class GuiDocEditFooter(QWidget):
|
||||
else:
|
||||
theCursor = self.docEditor.textCursor()
|
||||
iLine = theCursor.blockNumber() + 1
|
||||
iDist = 100*iLine/self.docEditor._qDocument.blockCount()
|
||||
iDist = 100*iLine/self.docEditor.document().blockCount()
|
||||
|
||||
self.linesText.setText(
|
||||
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
|
||||
@@ -2775,7 +2793,7 @@ class GuiDocEditFooter(QWidget):
|
||||
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
|
||||
)
|
||||
|
||||
byteSize = self.docEditor._qDocument.characterCount()
|
||||
byteSize = self.docEditor.document().characterCount()
|
||||
self.wordsText.setToolTip(
|
||||
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user