Improve editor stats handling and test coverage (#1725)

This commit is contained in:
Veronica Berglyd Olsen
2024-03-03 17:20:00 +01:00
committed by GitHub
5 changed files with 313 additions and 297 deletions
+6 -6
View File
@@ -351,16 +351,16 @@ class NWTree:
return False
return tItem.itemType == itemType
def getItemPath(self, tHandle: str) -> list[str]:
def getItemPath(self, tHandle: str, asName: bool = False) -> list[str]:
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
We do this with a for loop with a maximum depth to make
infinite loops impossible.
parent None, the root item, and return the list of handles, or
alternatively item names. We do this with a for loop with a
maximum depth to make infinite loops impossible.
"""
tTree = []
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
tTree.append(tItem.itemName if asName else tHandle)
for _ in range(MAX_DEPTH):
if tItem.itemParent is None:
return tTree
@@ -370,7 +370,7 @@ class NWTree:
if tItem is None:
return tTree
else:
tTree.append(tHandle)
tTree.append(tItem.itemName if asName else tHandle)
else:
raise RecursionError("Critical internal error")
+97 -165
View File
@@ -142,9 +142,10 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument = GuiTextDocument(self)
self.setDocument(self._qDocument)
# Connect Signals
# Connect Editor and Document Signals
self._qDocument.contentsChange.connect(self._docChange)
self.selectionChanged.connect(self._updateSelectedStatus)
self.cursorPositionChanged.connect(self._cursorMoved)
self.spellCheckStateChanged.connect(self._qDocument.setSpellCheckState)
# Document Title
@@ -153,7 +154,7 @@ class GuiDocEditor(QPlainTextEdit):
self.docSearch = GuiDocEditSearch(self)
self.docToolBar = GuiDocToolBar(self)
# Connect Signals
# Connect Widget Signals
self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument)
self.docHeader.toggleToolBarRequest.connect(self._toggleToolBarVisibility)
self.docToolBar.requestDocAction.connect(self.docAction)
@@ -400,8 +401,6 @@ class GuiDocEditor(QPlainTextEdit):
self.wcTimerDoc.start()
self.setReadOnly(False)
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.setHandle(self._docHandle)
self.updateDocMargins()
if tLine is None and self._nwItem is not None:
@@ -409,7 +408,8 @@ class GuiDocEditor(QPlainTextEdit):
elif isinstance(tLine, int):
self.setCursorLine(tLine)
self.docFooter.updateLineCount()
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.setHandle(self._docHandle)
# This is a hack to fix invisible cursor on an empty document
if self._qDocument.characterCount() <= 1:
@@ -616,7 +616,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(minmax(position, 0, nChars-1))
self.setTextCursor(cursor)
self.centerCursor()
self.docFooter.updateLineCount()
return
def saveCursorPosition(self) -> None:
@@ -949,8 +948,6 @@ class GuiDocEditor(QPlainTextEdit):
else:
super().keyPressEvent(event)
self.docFooter.updateLineCount()
return
def focusNextPrevChild(self, next: bool) -> bool:
@@ -973,7 +970,6 @@ class GuiDocEditor(QPlainTextEdit):
if qApp.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
self._processTag(self.cursorForPosition(event.pos()))
super().mouseReleaseEvent(event)
self.docFooter.updateLineCount()
return
def resizeEvent(self, event: QResizeEvent) -> None:
@@ -1038,29 +1034,32 @@ class GuiDocEditor(QPlainTextEdit):
if not self.wcTimerDoc.isActive():
self.wcTimerDoc.start()
block = self._qDocument.findBlock(pos)
if not block.isValid():
return
text = block.text()
if text.startswith("@") and added + removed == 1:
# Only run on single keypresses, otherwise it will trigger
# at unwanted times when other changes are made to the document
cursor = self.textCursor()
bPos = cursor.positionInBlock()
if bPos > 0:
show = self._completer.updateText(text, bPos)
point = self.cursorRect().bottomRight()
self._completer.move(self.viewport().mapToGlobal(point))
self._completer.setVisible(show)
else:
self._completer.setVisible(False)
if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text()
if text.startswith("@") and added + removed == 1:
# Only run on single keypresses, otherwise it will trigger
# at unwanted times when other changes are made to the document
cursor = self.textCursor()
bPos = cursor.positionInBlock()
if bPos > 0:
show = self._completer.updateText(text, bPos)
point = self.cursorRect().bottomRight()
self._completer.move(self.viewport().mapToGlobal(point))
self._completer.setVisible(show)
else:
self._completer.setVisible(False)
if self._doReplace and added == 1:
self._docAutoReplace(text)
return
@pyqtSlot()
def _cursorMoved(self):
"""Triggered when the cursor moved in the editor."""
self.docFooter.updateLineCount(self.textCursor())
return
@pyqtSlot(int, int, str)
def _insertCompletion(self, pos: int, length: int, text: str) -> None:
"""Insert choice from the completer menu."""
@@ -1198,19 +1197,13 @@ class GuiDocEditor(QPlainTextEdit):
@pyqtSlot(int, int, int)
def _updateDocCounts(self, cCount: int, wCount: int, pCount: int) -> None:
"""Process the word counter's finished signal."""
if self._docHandle is None or self._nwItem is None:
return
logger.debug("Updating word count")
self._nwItem.setCharCount(cCount)
self._nwItem.setWordCount(wCount)
self._nwItem.setParaCount(pCount)
# Must not be emitted if docHandle is None!
self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
self.docFooter.updateCounts()
if self._docHandle and self._nwItem:
logger.debug("Updating word count")
self._nwItem.setCharCount(cCount)
self._nwItem.setWordCount(wCount)
self._nwItem.setParaCount(pCount)
self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
self.docFooter.updateWordCount(wCount, False)
return
@pyqtSlot()
@@ -1221,11 +1214,9 @@ class GuiDocEditor(QPlainTextEdit):
if self.textCursor().hasSelection():
if not self.wcTimerSel.isActive():
self.wcTimerSel.start()
self.docFooter.setHasSelection(True)
else:
self.wcTimerSel.stop()
self.docFooter.setHasSelection(False)
self.docFooter.updateCounts()
self.docFooter.updateWordCount(0, False)
return
@pyqtSlot()
@@ -1245,13 +1236,10 @@ class GuiDocEditor(QPlainTextEdit):
@pyqtSlot(int, int, int)
def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
"""Update the counts on the counter's finished signal."""
if self._docHandle is None or self._nwItem is None:
return
logger.debug("User selected %d words", wCount)
self.docFooter.updateCounts(wCount=wCount, cCount=cCount)
self.wcTimerSel.stop()
if self._docHandle and self._nwItem:
logger.debug("User selected %d words", wCount)
self.docFooter.updateWordCount(wCount, True)
self.wcTimerSel.stop()
return
@pyqtSlot()
@@ -1341,7 +1329,6 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(resE[resIdx], QTextCursor.MoveMode.KeepAnchor)
self.setTextCursor(cursor)
self.docFooter.updateLineCount()
self.docSearch.setResultCount(resIdx + 1, len(resS))
self._lastFind = (resS[resIdx], resE[resIdx])
@@ -1438,13 +1425,7 @@ class GuiDocEditor(QPlainTextEdit):
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
isFind = self._lastFind[0] == cursor.selectionStart()
isFind &= self._lastFind[1] == cursor.selectionEnd()
except Exception:
isFind = False
if isFind:
if self._lastFind == (cursor.selectionStart(), cursor.selectionEnd()):
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(replWith)
@@ -1453,10 +1434,8 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor)
logger.debug(
"Replaced occurrence of '%s' with '%s' on line %d",
searchFor, replWith, cursor.blockNumber()
searchFor, replWith, cursor.blockNumber() + 1
)
else:
logger.error("The selected text is not a search result, skipping replace")
self.findNext()
@@ -2817,6 +2796,7 @@ class GuiDocEditHeader(QWidget):
self._docHandle = None
fPx = int(0.9*SHARED.theme.fontPixelSize)
mPx = CONFIG.pxInt(8)
hSp = CONFIG.pxInt(6)
iconSize = QSize(fPx, fPx)
@@ -2824,8 +2804,7 @@ class GuiDocEditHeader(QWidget):
self.setAutoFillBackground(True)
# Title Label
self.itemTitle = QLabel()
self.itemTitle.setText("")
self.itemTitle = QLabel("", self)
self.itemTitle.setIndent(0)
self.itemTitle.setMargin(0)
self.itemTitle.setContentsMargins(0, 0, 0, 0)
@@ -2882,14 +2861,14 @@ class GuiDocEditHeader(QWidget):
self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addWidget(self.minmaxButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setLayout(self.outerBox)
# Fix Margins and Size
# This is needed for high DPI systems. See issue #499.
cM = CONFIG.pxInt(8)
self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(cM, cM, cM, cM)
self.setMinimumHeight(fPx + 2*cM)
self.setMinimumHeight(fPx + 2*mPx)
self.updateTheme()
@@ -2937,7 +2916,7 @@ class GuiDocEditHeader(QWidget):
return
def setTitleFromHandle(self, tHandle: str | None) -> bool:
def setTitleFromHandle(self, tHandle: str | None) -> None:
"""Set the document title from the handle, or alternatively, set
the whole document path within the project.
"""
@@ -2948,30 +2927,21 @@ class GuiDocEditHeader(QWidget):
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
self.minmaxButton.setVisible(False)
return True
return
pTree = SHARED.project.tree
if CONFIG.showFullPath:
tTitle = []
tTree = pTree.getItemPath(tHandle)
for aHandle in reversed(tTree):
nwItem = pTree[aHandle]
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
self.itemTitle.setText(sSep.join(tTitle))
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
[name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
)))
else:
nwItem = pTree[tHandle]
if nwItem is None:
return False
self.itemTitle.setText(nwItem.itemName)
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
self.tbButton.setVisible(True)
self.searchButton.setVisible(True)
self.closeButton.setVisible(True)
self.minmaxButton.setVisible(True)
return True
return
def updateFocusMode(self) -> None:
"""Update the minimise/maximise icon of the Focus Mode button.
@@ -3007,7 +2977,7 @@ class GuiDocEditHeader(QWidget):
selected in the project tree.
"""
if event.button() == Qt.MouseButton.LeftButton:
self.docEditor.requestProjectItemSelected.emit(self._docHandle, True)
self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
return
# END Class GuiDocEditHeader
@@ -3020,26 +2990,28 @@ class GuiDocEditHeader(QWidget):
class GuiDocEditFooter(QWidget):
def __init__(self, docEditor: GuiDocEditor) -> None:
super().__init__(parent=docEditor)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
logger.debug("Create: GuiDocEditFooter")
self.docEditor = docEditor
self._tItem = None
self._tItem = None
self._docHandle = None
self._docSelection = False
self.sPx = int(round(0.9*SHARED.theme.baseIconSize))
iPx = round(0.9*SHARED.theme.baseIconSize)
fPx = int(0.9*SHARED.theme.fontPixelSize)
mPx = CONFIG.pxInt(8)
bSp = CONFIG.pxInt(4)
hSp = CONFIG.pxInt(6)
lblFont = self.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
# Cached Translations
self._trLineCount = self.tr("Line: {0} ({1})")
self._trWordCount = self.tr("Words: {0} ({1})")
self._trSelectCount = self.tr("Words: {0} selected")
# Main Widget Settings
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
@@ -3047,9 +3019,9 @@ class GuiDocEditFooter(QWidget):
alLeftTop = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
# Status
self.statusIcon = QLabel("")
self.statusIcon = QLabel("", self)
self.statusIcon.setContentsMargins(0, 0, 0, 0)
self.statusIcon.setFixedHeight(self.sPx)
self.statusIcon.setFixedHeight(iPx)
self.statusIcon.setAlignment(alLeftTop)
self.statusText = QLabel(self.tr("Status"))
@@ -3062,12 +3034,12 @@ class GuiDocEditFooter(QWidget):
self.statusText.setFont(lblFont)
# Lines
self.linesIcon = QLabel("")
self.linesIcon = QLabel("", self)
self.linesIcon.setContentsMargins(0, 0, 0, 0)
self.linesIcon.setFixedHeight(self.sPx)
self.linesIcon.setFixedHeight(iPx)
self.linesIcon.setAlignment(alLeftTop)
self.linesText = QLabel("")
self.linesText = QLabel("", self)
self.linesText.setIndent(0)
self.linesText.setMargin(0)
self.linesText.setContentsMargins(0, 0, 0, 0)
@@ -3077,12 +3049,12 @@ class GuiDocEditFooter(QWidget):
self.linesText.setFont(lblFont)
# Words
self.wordsIcon = QLabel("")
self.wordsIcon = QLabel("", self)
self.wordsIcon.setContentsMargins(0, 0, 0, 0)
self.wordsIcon.setFixedHeight(self.sPx)
self.wordsIcon.setFixedHeight(iPx)
self.wordsIcon.setAlignment(alLeftTop)
self.wordsText = QLabel("")
self.wordsText = QLabel("", self)
self.wordsText.setIndent(0)
self.wordsText.setMargin(0)
self.wordsText.setContentsMargins(0, 0, 0, 0)
@@ -3102,19 +3074,20 @@ class GuiDocEditFooter(QWidget):
self.outerBox.addSpacing(hSp)
self.outerBox.addWidget(self.wordsIcon)
self.outerBox.addWidget(self.wordsText)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setLayout(self.outerBox)
# Fix Margins and Size
# This is needed for high DPI systems. See issue #499.
cM = CONFIG.pxInt(8)
self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(cM, cM, cM, cM)
self.setMinimumHeight(fPx + 2*cM)
self.setMinimumHeight(fPx + 2*mPx)
# Fix the Colours
self.updateTheme()
self.updateLineCount()
self.updateCounts()
# Initialise Info
self.updateWordCount(0, False)
logger.debug("Ready: GuiDocEditFooter")
@@ -3126,8 +3099,9 @@ class GuiDocEditFooter(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (self.sPx, self.sPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (self.sPx, self.sPx)))
iPx = round(0.9*SHARED.theme.baseIconSize)
self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (iPx, iPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx)))
self.matchColours()
return
@@ -3156,27 +3130,20 @@ class GuiDocEditFooter(QWidget):
else:
self._tItem = SHARED.project.tree[self._docHandle]
self.setHasSelection(False)
self.updateInfo()
self.updateCounts()
self.updateWordCount(0, False)
return
def setHasSelection(self, hasSelection: bool) -> None:
"""Toggle the word counter mode between full count and selection
count mode.
"""
self._docSelection = hasSelection
return
def updateInfo(self) -> None:
"""Update the content of text labels."""
if self._tItem is None:
sIcon = QPixmap()
sText = ""
else:
iPx = round(0.9*SHARED.theme.baseIconSize)
status, icon = self._tItem.getImportStatus(incIcon=True)
sIcon = icon.pixmap(self.sPx, self.sPx)
sIcon = icon.pixmap(iPx, iPx)
sText = f"{status} / {self._tItem.describeMe()}"
self.statusIcon.setPixmap(sIcon)
@@ -3184,62 +3151,27 @@ class GuiDocEditFooter(QWidget):
return
def updateLineCount(self) -> None:
"""Update the line counter."""
if self._tItem is None:
iLine = 0
iDist = 0
else:
cursor = self.docEditor.textCursor()
iLine = cursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor._qDocument.blockCount()
def updateLineCount(self, cursor: QTextCursor) -> None:
"""Update the line and document position counter."""
cPos = cursor.position() + 1
cLine = cursor.blockNumber() + 1
cCount = max(cursor.document().characterCount(), 1)
self.linesText.setText(
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
)
return
def updateCounts(self, wCount: int | None = None, cCount: int | None = None) -> None:
"""Select which word count display mode to use."""
if self._docSelection:
self._updateSelectionWordCounts(wCount, cCount)
else:
self._updateWordCounts()
return
##
# Internal Functions
##
def _updateWordCounts(self) -> None:
"""Update the word count for the whole document."""
if self._tItem is None:
wCount = 0
wDiff = 0
else:
def updateWordCount(self, wCount: int, selection: bool) -> None:
"""Update word counter information."""
if selection and wCount:
wText = self._trSelectCount.format(f"{wCount:n}")
elif self._tItem:
wCount = self._tItem.wordCount
wDiff = wCount - self._tItem.initCount
self.wordsText.setText(
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
)
byteSize = self.docEditor._qDocument.characterCount()
self.wordsText.setToolTip(
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
)
return
def _updateSelectionWordCounts(self, wCount: int | None, cCount: int | None) -> None:
"""Update the word count for a selection."""
if wCount is None or cCount is None:
return
self.wordsText.setText(
self.tr("Words: {0} selected").format(f"{wCount:n}")
)
self.wordsText.setToolTip(
self.tr("Character count: {0}").format(f"{cCount:n}")
)
wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}")
else:
wText = self._trWordCount.format("0", "+0")
self.wordsText.setText(wText)
return
# END Class GuiDocEditFooter
+4 -11
View File
@@ -763,19 +763,12 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(False)
return
pTree = SHARED.project.tree
if CONFIG.showFullPath:
tTitle = []
tTree = pTree.getItemPath(tHandle)
for aHandle in reversed(tTree):
nwItem = pTree[aHandle]
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
self.docTitle.setText(sSep.join(tTitle))
self.docTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
[name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
)))
else:
if nwItem := pTree[tHandle]:
self.docTitle.setText(nwItem.itemName)
self.docTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
self.backButton.setVisible(True)
self.forwardButton.setVisible(True)
+3
View File
@@ -421,6 +421,9 @@ def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
assert tree.getItemPath("c000000000001") == [
"c000000000001", "b000000000001", "a000000000001"
]
assert tree.getItemPath("c000000000001", asName=True) == [
"Chapter One", "Act One", "Novel"
]
# Cause recursion error
with monkeypatch.context() as mp:
+203 -115
View File
@@ -55,6 +55,9 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.docEditor.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert nwGUI.docEditor.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert nwGUI.docEditor._typPadChar == nwUnicode.U_NBSP
assert nwGUI.docEditor.docHeader.itemTitle.text() == (
"Novel \u203a New Chapter \u203a New Scene"
)
# Check that editor handles settings
CONFIG.textFont = ""
@@ -64,6 +67,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
CONFIG.hideVScroll = True
CONFIG.hideHScroll = True
CONFIG.fmtPadThin = True
CONFIG.showFullPath = False
nwGUI.docEditor.initEditor()
@@ -75,6 +79,29 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.docEditor.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert nwGUI.docEditor.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert nwGUI.docEditor._typPadChar == nwUnicode.U_THNBSP
assert nwGUI.docEditor.docHeader.itemTitle.text() == "New Scene"
# Header
# ======
# Select item from header
with qtbot.waitSignal(nwGUI.docEditor.requestProjectItemSelected, timeout=1000) as signal:
qtbot.mouseClick(nwGUI.docEditor.docHeader, Qt.MouseButton.LeftButton)
assert signal.args == [nwGUI.docEditor.docHeader._docHandle, True]
# Close from header
with qtbot.waitSignal(nwGUI.docEditor.docHeader.closeDocumentRequest, timeout=1000):
nwGUI.docEditor.docHeader.closeButton.click()
assert nwGUI.docEditor.docHeader.tbButton.isVisible() is False
assert nwGUI.docEditor.docHeader.searchButton.isVisible() is False
assert nwGUI.docEditor.docHeader.closeButton.isVisible() is False
assert nwGUI.docEditor.docHeader.minmaxButton.isVisible() is False
# Select item from header
with qtbot.waitSignal(nwGUI.docEditor.requestProjectItemSelected, timeout=1000) as signal:
qtbot.mouseClick(nwGUI.docEditor.docHeader, Qt.MouseButton.LeftButton)
assert signal.args == ["", True]
# qtbot.stop()
@@ -82,7 +109,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
@pytest.mark.gui
def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumText, mockRnd):
def testGuiEditor_LoadText(qtbot, nwGUI, projPath, ipsumText, mockRnd):
"""Test loading text into the editor."""
buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True
@@ -92,9 +119,6 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex
assert nwGUI.saveDocument() is True
assert nwGUI.closeDocument() is True
# Load Text
# =========
# Invalid handle
assert nwGUI.docEditor.loadText("abcdefghijklm") is False
@@ -123,9 +147,6 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex
buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True
# Save Text
# =========
longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText)
nwGUI.docEditor.replaceText(longText)
@@ -1698,17 +1719,12 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
assert SHARED.project.tree[C.hSceneDoc]._paraCount == pC # type: ignore
assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})"
# Select all text
assert nwGUI.docEditor.docFooter._docSelection is False
# Select all text and run the selection word counter
nwGUI.docEditor.docAction(nwDocAction.SEL_ALL)
assert nwGUI.docEditor.docFooter._docSelection is True
# Run the selection word counter
nwGUI.docEditor._runSelCounter()
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
nwGUI.docEditor.wCounterSel.run()
# nwGUI.docEditor._updateSelCounts(cC, wC, pC)
assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} selected"
# qtbot.stop()
@@ -1723,201 +1739,273 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.openProject(prjLipsum) is True
assert nwGUI.openDocument("4c4f28287af27") is True
origText = nwGUI.docEditor.getText()
docEditor = nwGUI.docEditor
docSearch = docEditor.docSearch
origText = docEditor.getText()
# Select the Word "est"
nwGUI.docEditor.setCursorPosition(645)
nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor)
cursor = nwGUI.docEditor.textCursor()
docEditor.setCursorPosition(645)
docEditor._makeSelection(QTextCursor.WordUnderCursor)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
# Activate search
nwGUI.mainMenu.aFind.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.isVisible()
assert nwGUI.docEditor.docSearch.searchText == "est"
assert docSearch.isVisible()
assert docSearch.searchText == "est"
# Find next by enter key
monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: True)
qtbot.keyClick(nwGUI.docEditor.docSearch.searchBox, Qt.Key_Return, delay=KEY_DELAY)
assert abs(nwGUI.docEditor.getCursorPosition() - 1299) < 3
monkeypatch.setattr(docSearch.searchBox, "hasFocus", lambda: True)
qtbot.keyClick(docSearch.searchBox, Qt.Key_Return, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1299) < 3
# Find next by button
qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(nwGUI.docEditor.getCursorPosition() - 1513) < 3
qtbot.mouseClick(docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1513) < 3
# Activate loop search
nwGUI.docEditor.docSearch.toggleLoop.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleLoop.isChecked()
assert nwGUI.docEditor.docSearch.doLoop is True
docSearch.toggleLoop.activate(QAction.Trigger)
assert docSearch.toggleLoop.isChecked()
assert docSearch.doLoop is True
# Find next by menu Search > Find Next
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 647) < 3
# Close search
nwGUI.docEditor.docSearch.cancelSearch.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.isVisible() is False
nwGUI.docEditor.setCursorPosition(15)
docSearch.cancelSearch.activate(QAction.Trigger)
assert docSearch.isVisible() is False
docEditor.setCursorPosition(15)
# Toggle search again with header button
qtbot.mouseClick(nwGUI.docEditor.docHeader.searchButton, Qt.LeftButton, delay=KEY_DELAY)
nwGUI.docEditor.docSearch.setSearchText("")
assert nwGUI.docEditor.docSearch.isVisible() is True
qtbot.mouseClick(docEditor.docHeader.searchButton, Qt.LeftButton, delay=KEY_DELAY)
docSearch.setSearchText("")
assert docSearch.isVisible() is True
# Search for non-existing
nwGUI.docEditor.setCursorPosition(0)
nwGUI.docEditor.docSearch.setSearchText("abcdef")
qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert nwGUI.docEditor.getCursorPosition() < 3 # No result
docEditor.setCursorPosition(0)
docSearch.setSearchText("abcdef")
qtbot.mouseClick(docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert docEditor.getCursorPosition() < 3 # No result
# Enable RegEx search
nwGUI.docEditor.docSearch.toggleRegEx.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleRegEx.isChecked()
assert nwGUI.docEditor.docSearch.isRegEx is True
docSearch.toggleRegEx.activate(QAction.Trigger)
assert docSearch.toggleRegEx.isChecked()
assert docSearch.isRegEx is True
# Set invalid RegEx
nwGUI.docEditor.setCursorPosition(0)
nwGUI.docEditor.docSearch.setSearchText(r"\bSus[")
qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert nwGUI.docEditor.getCursorPosition() < 3 # No result
docEditor.setCursorPosition(0)
docSearch.setSearchText(r"\bSus[")
qtbot.mouseClick(docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert docEditor.getCursorPosition() < 3 # No result
# Set dangerous RegEx (issue #1015)
# If this doesn't get caught, the app will hang
nwGUI.docEditor.setCursorPosition(0)
nwGUI.docEditor.docSearch.setSearchText(r".*")
qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(nwGUI.docEditor.getCursorPosition() - 14) < 3
docEditor.setCursorPosition(0)
docSearch.setSearchText(r".*")
qtbot.mouseClick(docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 14) < 3
# Set valid RegEx
nwGUI.docEditor.docSearch.setSearchText(r"\bSus")
qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(nwGUI.docEditor.getCursorPosition() - 223) < 3
docSearch.setSearchText(r"\bSus")
qtbot.mouseClick(docSearch.searchButton, Qt.LeftButton, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 223) < 3
# Find next and then prev
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 324) < 3
assert abs(docEditor.getCursorPosition() - 324) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 223) < 3
# Make RegEx case sensitive
nwGUI.docEditor.docSearch.toggleCase.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleCase.isChecked()
assert nwGUI.docEditor.docSearch.isCaseSense is True
docSearch.toggleCase.activate(QAction.Trigger)
assert docSearch.toggleCase.isChecked()
assert docSearch.isCaseSense is True
# Find next/prev (one result)
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 626) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 626) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 626) < 3
# Trigger replace
nwGUI.mainMenu.aReplace.activate(QAction.Trigger)
nwGUI.docEditor.docSearch.setReplaceText("foo")
docSearch.setReplaceText("foo")
# Disable RegEx case sensitive
nwGUI.docEditor.docSearch.toggleCase.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleCase.isChecked() is False
assert nwGUI.docEditor.docSearch.isCaseSense is False
docSearch.toggleCase.activate(QAction.Trigger)
assert docSearch.toggleCase.isChecked() is False
assert docSearch.isCaseSense is False
# Toggle replace preserve case
nwGUI.docEditor.docSearch.toggleMatchCap.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleMatchCap.isChecked()
assert nwGUI.docEditor.docSearch.doMatchCap is True
docSearch.toggleMatchCap.activate(QAction.Trigger)
assert docSearch.toggleMatchCap.isChecked()
assert docSearch.doMatchCap is True
# Replace "Sus" with "Foo" via menu
nwGUI.docEditor.setCursorPosition(605)
docEditor.setCursorPosition(605)
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
nwGUI.mainMenu.aReplaceNext.activate(QAction.Trigger)
assert nwGUI.docEditor.getText()[623:634] == "Foopendisse"
assert docEditor.getText()[623:634] == "Foopendisse"
# Find next/prev to loop file
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 223) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 1805) < 3
assert abs(docEditor.getCursorPosition() - 1805) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 223) < 3
# Replace "sus" with "foo" via replace button
qtbot.mouseClick(nwGUI.docEditor.docSearch.replaceButton, Qt.LeftButton, delay=KEY_DELAY)
assert nwGUI.docEditor.getText()[220:228] == "foocipit"
qtbot.mouseClick(docSearch.replaceButton, Qt.LeftButton, delay=KEY_DELAY)
assert docEditor.getText()[220:228] == "foocipit"
# Revert last two replaces
assert nwGUI.docEditor.docAction(nwDocAction.UNDO)
assert nwGUI.docEditor.docAction(nwDocAction.UNDO)
assert nwGUI.docEditor.getText() == origText
assert docEditor.docAction(nwDocAction.UNDO)
assert docEditor.docAction(nwDocAction.UNDO)
assert docEditor.getText() == origText
# Disable RegEx search
nwGUI.docEditor.docSearch.toggleRegEx.activate(QAction.Trigger)
assert not nwGUI.docEditor.docSearch.toggleRegEx.isChecked()
assert nwGUI.docEditor.docSearch.isRegEx is False
docSearch.toggleRegEx.activate(QAction.Trigger)
assert not docSearch.toggleRegEx.isChecked()
assert docSearch.isRegEx is False
# Close search and select "est" again
nwGUI.docEditor.docSearch.cancelSearch.activate(QAction.Trigger)
nwGUI.docEditor.setCursorPosition(645)
nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor)
cursor = nwGUI.docEditor.textCursor()
docSearch.cancelSearch.activate(QAction.Trigger)
docEditor.setCursorPosition(645)
docEditor._makeSelection(QTextCursor.WordUnderCursor)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
# Activate search again
nwGUI.mainMenu.aFind.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.isVisible()
assert nwGUI.docEditor.docSearch.searchText == "est"
assert docSearch.isVisible()
assert docSearch.searchText == "est"
# Enable full word search
nwGUI.docEditor.docSearch.toggleWord.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleWord.isChecked()
assert nwGUI.docEditor.docSearch.isWholeWord is True
docSearch.toggleWord.activate(QAction.Trigger)
assert docSearch.toggleWord.isChecked()
assert docSearch.isWholeWord is True
# Only one match
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 647) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 647) < 3
# Enable next doc search
nwGUI.docEditor.docSearch.toggleProject.activate(QAction.Trigger)
assert nwGUI.docEditor.docSearch.toggleProject.isChecked()
assert nwGUI.docEditor.docSearch.doNextFile is True
docSearch.toggleProject.activate(QAction.Trigger)
assert docSearch.toggleProject.isChecked()
assert docSearch.doNextFile is True
# Next match
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle == "2426c6f0ca922" # Next document
assert docEditor.docHandle == "2426c6f0ca922" # Next document
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3
assert abs(docEditor.getCursorPosition() - 620) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 1127) < 3
assert abs(docEditor.getCursorPosition() - 1127) < 3
# Next doc, no match
assert nwGUI.docEditor.docSearch.doNextFile is True
nwGUI.docEditor.docSearch.setSearchText("abcdef")
assert docSearch.doNextFile is True
docSearch.setSearchText("abcdef")
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle != "2426c6f0ca922"
assert nwGUI.docEditor.docHandle == "04468803b92e1"
assert docEditor.docHandle != "2426c6f0ca922"
assert docEditor.docHandle == "04468803b92e1"
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle != "04468803b92e1"
assert nwGUI.docEditor.docHandle == "7a992350f3eb6"
assert docEditor.docHandle != "04468803b92e1"
assert docEditor.docHandle == "7a992350f3eb6"
# Toggle Replace
nwGUI.docEditor.beginReplace()
docEditor.beginReplace()
# MonkeyPatch the focus cycle. We can't really test this very well, other than
# check that the tabs aren't captured when the main editor has focus
monkeypatch.setattr(nwGUI.docEditor, "hasFocus", lambda: True)
monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: False)
monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: False)
assert nwGUI.docEditor.focusNextPrevChild(True) is False
with monkeypatch.context() as mp:
mp.setattr(docEditor, "hasFocus", lambda: True)
mp.setattr(docSearch.searchBox, "hasFocus", lambda: False)
mp.setattr(docSearch.replaceBox, "hasFocus", lambda: False)
assert docEditor.focusNextPrevChild(True) is False
assert docSearch.cycleFocus(True) is False
monkeypatch.setattr(nwGUI.docEditor, "hasFocus", lambda: False)
monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: True)
monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: False)
assert nwGUI.docEditor.focusNextPrevChild(True) is True
with monkeypatch.context() as mp:
mp.setattr(docEditor, "hasFocus", lambda: False)
mp.setattr(docSearch.searchBox, "hasFocus", lambda: True)
mp.setattr(docSearch.replaceBox, "hasFocus", lambda: False)
assert docEditor.focusNextPrevChild(True) is True
assert docSearch.cycleFocus(True) is True
monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: False)
monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: True)
assert nwGUI.docEditor.focusNextPrevChild(True) is True
with monkeypatch.context() as mp:
mp.setattr(docEditor, "hasFocus", lambda: False)
mp.setattr(docSearch.searchBox, "hasFocus", lambda: False)
mp.setattr(docSearch.replaceBox, "hasFocus", lambda: True)
assert docEditor.focusNextPrevChild(True) is True
assert docSearch.cycleFocus(True) is True
docSearch.closeSearch()
assert docSearch.isVisible() is False
assert docEditor.focusNextPrevChild(True) is True
# Replace Text
# ============
docSearch.toggleCase.setChecked(True)
docSearch.toggleWord.setChecked(False)
docSearch.toggleRegEx.setChecked(False)
docSearch.toggleLoop.setChecked(False)
docSearch.toggleProject.setChecked(False)
docEditor.setCursorPosition(0)
# Replace Next
docSearch.searchBox.setText("a")
docSearch.replaceBox.setText("A")
# No focus
with monkeypatch.context() as mp:
mp.setattr(docEditor, "anyFocus", lambda: False)
docEditor.findNext()
assert docEditor.textCursor().selectedText() == ""
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == ""
# Search not open
docSearch.closeSearch()
assert docSearch.isVisible() is False
docEditor.findNext()
assert docSearch.isVisible() is True
docSearch.closeSearch()
assert docSearch.isVisible() is False
docEditor.replaceNext()
assert docSearch.isVisible() is True
docEditor.toggleSearch()
assert docSearch.isVisible() is False
docEditor.toggleSearch()
assert docSearch.isVisible() is True
# Find first entry
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == 64
# Treat the search as a user selection
docEditor._lastFind = None
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == 92
# Iterate through the rest
finds = [104, 123, 175, 197, 206, 211, 220, 238, 250, 250]
for i in range(10):
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == finds[i]
assert docEditor._lastFind == (249, 250)
# Search for something that doesn't exist
docSearch.searchBox.setText("x")
docEditor._lastFind = None
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == ""
# qtbot.stop()