From ef9e84f2cecbb337fd48d733fc91590904a2ffe0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:00:12 +0100 Subject: [PATCH 1/5] Clean up the line counter logic in the editor --- novelwriter/gui/doceditor.py | 86 +++++++++++++++--------------------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e715ca8f..52080f32 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -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: @@ -1061,6 +1057,12 @@ class GuiDocEditor(QPlainTextEdit): 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.""" @@ -1341,7 +1343,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]) @@ -3020,13 +3021,11 @@ 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._docHandle = None @@ -3047,7 +3046,7 @@ 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.setAlignment(alLeftTop) @@ -3062,12 +3061,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.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 +3076,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.setAlignment(alLeftTop) - self.wordsText = QLabel("") + self.wordsText = QLabel("", self) self.wordsText.setIndent(0) self.wordsText.setMargin(0) self.wordsText.setContentsMargins(0, 0, 0, 0) @@ -3113,7 +3112,6 @@ class GuiDocEditFooter(QWidget): # Fix the Colours self.updateTheme() - self.updateLineCount() self.updateCounts() logger.debug("Ready: GuiDocEditFooter") @@ -3184,17 +3182,17 @@ class GuiDocEditFooter(QWidget): return - def updateLineCount(self) -> None: + def updateLineCount(self, cursor: QTextCursor) -> 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() + cPos = cursor.position() + 1 + cCount = max(cursor.document().characterCount(), 1) + iLine = cursor.blockNumber() + 1 + iDist = 100*cPos//cCount 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:d} %") + ) + self.linesText.setToolTip( + self.tr("Document size is {0} bytes").format(f"{cCount:n}") ) return @@ -3212,34 +3210,22 @@ class GuiDocEditFooter(QWidget): def _updateWordCounts(self) -> None: """Update the word count for the whole document.""" - if self._tItem is None: - wCount = 0 - wDiff = 0 - else: - wCount = self._tItem.wordCount - wDiff = wCount - self._tItem.initCount - + wCount = self._tItem.wordCount if self._tItem else 0 + wDiff = wCount - self._tItem.initCount if self._tItem else 0 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}") - ) + if wCount and cCount: + self.wordsText.setText( + self.tr("Words: {0} selected").format(f"{wCount:n}") + ) + self.wordsText.setToolTip( + self.tr("Character count: {0}").format(f"{cCount:n}") + ) return # END Class GuiDocEditFooter From 79513bcbe547e40a2e1061a0711f8a60465869fe Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:39:08 +0100 Subject: [PATCH 2/5] Improve the word counter logic of the editor --- novelwriter/gui/doceditor.py | 140 ++++++++++----------------- tests/test_gui/test_gui_doceditor.py | 7 +- 2 files changed, 54 insertions(+), 93 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 52080f32..ba738360 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1200,19 +1200,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() @@ -1223,11 +1217,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() @@ -1247,13 +1239,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() @@ -2818,6 +2807,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) @@ -2825,8 +2815,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) @@ -2883,14 +2872,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() @@ -3026,19 +3015,23 @@ class GuiDocEditFooter(QWidget): logger.debug("Create: GuiDocEditFooter") - 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) @@ -3048,7 +3041,7 @@ class GuiDocEditFooter(QWidget): # Status 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")) @@ -3063,7 +3056,7 @@ class GuiDocEditFooter(QWidget): # Lines 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) @@ -3078,7 +3071,7 @@ class GuiDocEditFooter(QWidget): # Words 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) @@ -3101,18 +3094,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.updateCounts() + + # Initialise Info + self.updateWordCount(0, False) logger.debug("Ready: GuiDocEditFooter") @@ -3124,8 +3119,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 @@ -3154,27 +3150,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) @@ -3183,49 +3172,26 @@ class GuiDocEditFooter(QWidget): return def updateLineCount(self, cursor: QTextCursor) -> None: - """Update the line counter.""" + """Update the line and document position counter.""" cPos = cursor.position() + 1 + cLine = cursor.blockNumber() + 1 cCount = max(cursor.document().characterCount(), 1) - iLine = cursor.blockNumber() + 1 - iDist = 100*cPos//cCount self.linesText.setText( - self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:d} %") - ) - self.linesText.setToolTip( - self.tr("Document size is {0} bytes").format(f"{cCount:n}") + 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) + 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 + wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}") else: - self._updateWordCounts() - return - - ## - # Internal Functions - ## - - def _updateWordCounts(self) -> None: - """Update the word count for the whole document.""" - wCount = self._tItem.wordCount if self._tItem else 0 - wDiff = wCount - self._tItem.initCount if self._tItem else 0 - self.wordsText.setText( - self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}") - ) - return - - def _updateSelectionWordCounts(self, wCount: int | None, cCount: int | None) -> None: - """Update the word count for a selection.""" - if wCount and cCount: - 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("0", "+0") + self.wordsText.setText(wText) return # END Class GuiDocEditFooter diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 419e4bf9..1cffea70 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1698,17 +1698,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() From 57b3aa20320f1589462c00d0bb193e72eb4eeb0e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:30:48 +0100 Subject: [PATCH 3/5] Improve handling of title in doc headers --- novelwriter/core/tree.py | 12 ++++++------ novelwriter/gui/doceditor.py | 25 ++++++++----------------- novelwriter/gui/docviewer.py | 15 ++++----------- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 3624bb9d..3e0a1b00 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -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") diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index ba738360..5db38217 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2927,7 +2927,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. """ @@ -2938,30 +2938,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. @@ -2997,7 +2988,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 diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 52a07f12..f84b727e 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -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) From 8859fc7b6b73a305e8489b8df71f14d8db923455 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:31:02 +0100 Subject: [PATCH 4/5] Improve test coverage --- tests/test_core/test_core_tree.py | 3 +++ tests/test_gui/test_gui_doceditor.py | 35 ++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 8cd9ad95..9e6e146e 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -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: diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 1cffea70..e5a79d21 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -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) From d97ce592f8abe4e89e6b14485034a5298391e4f5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:14:46 +0100 Subject: [PATCH 5/5] Improve test coverage of editor search --- novelwriter/gui/doceditor.py | 43 ++--- tests/test_gui/test_gui_doceditor.py | 276 +++++++++++++++++---------- 2 files changed, 190 insertions(+), 129 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 5db38217..ed0ba5fe 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1034,23 +1034,20 @@ 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) @@ -1428,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) @@ -1443,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() diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index e5a79d21..c8b1d581 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1739,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()