From e4206bdcc075d9f7bd7b2840efa9e4e5ff53202b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Nov 2023 19:44:24 +0100 Subject: [PATCH 1/5] Make autoselect and formatting selection a bit smarter --- novelwriter/gui/doceditor.py | 96 +++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index d03302ab..2bbef05c 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -67,6 +67,16 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) +class SelectAction(Enum): + + NO_DECISION = 0 + KEEP_SELECTION = 1 + KEEP_POSITION = 2 + MOVE_AFTER = 3 + +# END Class EditorSelectMode + + class GuiDocEditor(QPlainTextEdit): """Gui Widget: Main Document Editor""" @@ -1410,17 +1420,28 @@ class GuiDocEditor(QPlainTextEdit): If more than one block is selected, the formatting is applied to the first block. """ - cursor = self._autoSelect() - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False + cursor = self.textCursor() + posO = cursor.position() + if cursor.hasSelection(): + select = SelectAction.KEEP_SELECTION + else: + cursor = self._autoSelect() + if cursor.hasSelection() and posO == cursor.selectionEnd(): + select = SelectAction.MOVE_AFTER + else: + select = SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() + if self._qDocument.characterAt(posO - 1) == fChar: + logger.warning("Format repetition, cancelling action") + cursor.clearSelection() + cursor.setPosition(posO) + self.setTextCursor(cursor) + return False blockS = self._qDocument.findBlock(posS) blockE = self._qDocument.findBlock(posE) - if blockS != blockE: posE = blockS.position() + blockS.length() - 1 cursor.clearSelection() @@ -1443,34 +1464,26 @@ class GuiDocEditor(QPlainTextEdit): break if fLen == min(numA, numB): - self._clearSurrounding(cursor, fLen) + cursor.clearSelection() + cursor.beginEditBlock() + cursor.setPosition(posS) + for i in range(fLen): + cursor.deletePreviousChar() + cursor.setPosition(posE) + for i in range(fLen): + cursor.deletePreviousChar() + cursor.endEditBlock() + cursor.clearSelection() + cursor.setPosition(posO - fLen) + self.setTextCursor(cursor) + else: - self._wrapSelection(fChar*fLen) + self._wrapSelection(fChar*fLen, pos=posO, select=select) return True - def _clearSurrounding(self, cursor: QTextCursor, nChars: int) -> bool: - """Clear n characters before and after the cursor.""" - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False - - posS = cursor.selectionStart() - posE = cursor.selectionEnd() - cursor.clearSelection() - cursor.beginEditBlock() - cursor.setPosition(posS) - for i in range(nChars): - cursor.deletePreviousChar() - cursor.setPosition(posE) - for i in range(nChars): - cursor.deletePreviousChar() - cursor.endEditBlock() - cursor.clearSelection() - - return True - - def _wrapSelection(self, before: str, after: str | None = None) -> bool: + def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None, + select: SelectAction = SelectAction.NO_DECISION) -> bool: """Wrap the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the action. AutoSelect will select the word under the cursor before @@ -1479,10 +1492,17 @@ class GuiDocEditor(QPlainTextEdit): if after is None: after = before - cursor = self._autoSelect() - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False + cursor = self.textCursor() + posO = pos if isinstance(pos, int) else cursor.position() + if select == SelectAction.NO_DECISION: + if cursor.hasSelection(): + select = SelectAction.KEEP_SELECTION + else: + cursor = self._autoSelect() + if cursor.hasSelection() and posO == cursor.selectionEnd(): + select = SelectAction.MOVE_AFTER + else: + select = SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() @@ -1500,8 +1520,14 @@ class GuiDocEditor(QPlainTextEdit): cursor.insertText(before) cursor.endEditBlock() - cursor.setPosition(posE + len(before), QTextCursor.MoveAnchor) - cursor.setPosition(posS + len(before), QTextCursor.KeepAnchor) + if select == SelectAction.MOVE_AFTER: + cursor.setPosition(posE + len(before + after)) + elif select == SelectAction.KEEP_SELECTION: + cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor) + elif select == SelectAction.KEEP_POSITION: + cursor.setPosition(posO + len(before)) + self.setTextCursor(cursor) return True From 597b7882545f38b8ecc7fbba150fd33f94606a26 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:11:31 +0100 Subject: [PATCH 2/5] Implement a new word auto select feature that is more careful with punctuation --- novelwriter/constants.py | 3 +-- novelwriter/gui/doceditor.py | 46 +++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 5579e8a3..514ea9f2 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -327,8 +327,7 @@ class nwQuotes: class nwUnicode: - """Supported unicode character constants and their HTML equivalents. - """ + """Supported unicode character constants and their HTML equivalents.""" # Unicode Constants # ================= diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 2bbef05c..3fdc83cf 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1934,27 +1934,39 @@ class GuiDocEditor(QPlainTextEdit): def _autoSelect(self) -> QTextCursor: """Return a cursor which may or may not have a selection based - on user settings and document action. + on user settings and document action. The selection will be the + word closest to the cursor consisting of alphanumerical unicode + characters. """ cursor = self.textCursor() if CONFIG.autoSelect and not cursor.hasSelection(): - cursor.select(QTextCursor.WordUnderCursor) - posS = cursor.selectionStart() - posE = cursor.selectionEnd() + cPos = cursor.position() + bPos = cursor.block().position() + bLen = cursor.block().length() - # 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: - cursor.clearSelection() - cursor.setPosition(posS, QTextCursor.MoveAnchor) - cursor.setPosition(posE-1, QTextCursor.KeepAnchor) + # Scan backwards + sPos = cPos + for i in range(cPos - bPos): + sPos = cPos - i - 1 + if not self._qDocument.characterAt(sPos).isalnum(): + sPos += 1 + break + + # Scan forwards + ePos = cPos + for i in range(bPos + bLen - cPos): + ePos = cPos + i + if not self._qDocument.characterAt(ePos).isalnum(): + # ePos -= 1 + break + + if ePos - sPos <= 0: + # No selection possible + return cursor + + cursor.clearSelection() + cursor.setPosition(sPos, QTextCursor.MoveAnchor) + cursor.setPosition(ePos, QTextCursor.KeepAnchor) self.setTextCursor(cursor) From 7cf7eb4a48e58cbd2c49dae36aef053b0106fce0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:21:35 +0100 Subject: [PATCH 3/5] Fix test coverage --- tests/test_gui/test_gui_doceditor.py | 50 ++++------------------------ 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 064fcf17..008ad053 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -672,7 +672,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd @pytest.mark.gui -def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): +def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the text manipulation functions.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True @@ -680,37 +680,6 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) nwGUI.docEditor.replaceText(text) - # Clear Surrounding - # ================= - - # No Selection - text = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(text) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - assert nwGUI.docEditor._clearSurrounding(cursor, 1) is False - - # Clear Characters, 1 Layer - repText = text.replace("consectetur", "=consectetur=") - nwGUI.docEditor.replaceText(repText) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - cursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(cursor, 1) is True - assert nwGUI.docEditor.getText() == text - - # Clear Characters, 2 Layers - repText = text.replace("consectetur", "==consectetur==") - nwGUI.docEditor.replaceText(repText) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - cursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(cursor, 2) is True - assert nwGUI.docEditor.getText() == text - # Wrap Selection # ============== @@ -718,11 +687,6 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - # No Selection - with monkeypatch.context() as mp: - mp.setattr(nwGUI.docEditor, "_autoSelect", lambda: QTextCursor()) - assert nwGUI.docEditor._wrapSelection("=", "=") is False - # Wrap Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) @@ -752,13 +716,13 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex # ============= text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) - nwGUI.docEditor.replaceText(text) - nwGUI.docEditor.setCursorPosition(45) - # No Selection - with monkeypatch.context() as mp: - mp.setattr(nwGUI.docEditor, "_autoSelect", lambda: QTextCursor()) - assert nwGUI.docEditor._toggleFormat(2, "=") is False + # Block format repetition + nwGUI.docEditor.replaceText(text) + nwGUI.docEditor.setCursorPosition(39) + assert nwGUI.docEditor._toggleFormat(1, "=") is True + assert nwGUI.docEditor.getText() == text.replace("amet", "=amet=", 1) + assert nwGUI.docEditor._toggleFormat(1, "=") is False # Wrap Single Equal nwGUI.docEditor.replaceText(text) From 37e031959867369c29ca7b96918cbbe342e280c6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:38:03 +0100 Subject: [PATCH 4/5] Clean up code a bit --- novelwriter/gui/doceditor.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 3fdc83cf..b380da39 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -67,7 +67,7 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) -class SelectAction(Enum): +class _SelectAction(Enum): NO_DECISION = 0 KEEP_SELECTION = 1 @@ -1423,13 +1423,13 @@ class GuiDocEditor(QPlainTextEdit): cursor = self.textCursor() posO = cursor.position() if cursor.hasSelection(): - select = SelectAction.KEEP_SELECTION + select = _SelectAction.KEEP_SELECTION else: cursor = self._autoSelect() if cursor.hasSelection() and posO == cursor.selectionEnd(): - select = SelectAction.MOVE_AFTER + select = _SelectAction.MOVE_AFTER else: - select = SelectAction.KEEP_POSITION + select = _SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() @@ -1483,7 +1483,7 @@ class GuiDocEditor(QPlainTextEdit): return True def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None, - select: SelectAction = SelectAction.NO_DECISION) -> bool: + select: _SelectAction = _SelectAction.NO_DECISION) -> bool: """Wrap the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the action. AutoSelect will select the word under the cursor before @@ -1494,15 +1494,15 @@ class GuiDocEditor(QPlainTextEdit): cursor = self.textCursor() posO = pos if isinstance(pos, int) else cursor.position() - if select == SelectAction.NO_DECISION: + if select == _SelectAction.NO_DECISION: if cursor.hasSelection(): - select = SelectAction.KEEP_SELECTION + select = _SelectAction.KEEP_SELECTION else: cursor = self._autoSelect() if cursor.hasSelection() and posO == cursor.selectionEnd(): - select = SelectAction.MOVE_AFTER + select = _SelectAction.MOVE_AFTER else: - select = SelectAction.KEEP_POSITION + select = _SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() @@ -1520,12 +1520,12 @@ class GuiDocEditor(QPlainTextEdit): cursor.insertText(before) cursor.endEditBlock() - if select == SelectAction.MOVE_AFTER: + if select == _SelectAction.MOVE_AFTER: cursor.setPosition(posE + len(before + after)) - elif select == SelectAction.KEEP_SELECTION: + elif select == _SelectAction.KEEP_SELECTION: cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor) cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor) - elif select == SelectAction.KEEP_POSITION: + elif select == _SelectAction.KEEP_POSITION: cursor.setPosition(posO + len(before)) self.setTextCursor(cursor) @@ -1957,7 +1957,6 @@ class GuiDocEditor(QPlainTextEdit): for i in range(bPos + bLen - cPos): ePos = cPos + i if not self._qDocument.characterAt(ePos).isalnum(): - # ePos -= 1 break if ePos - sPos <= 0: From 45b278f999a8cff7b02b38b93c933a2e4b431d21 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:38:38 +0100 Subject: [PATCH 5/5] Fix an outdated comment --- novelwriter/gui/doceditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b380da39..6a1d2585 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -74,7 +74,7 @@ class _SelectAction(Enum): KEEP_POSITION = 2 MOVE_AFTER = 3 -# END Class EditorSelectMode +# END Class _SelectAction class GuiDocEditor(QPlainTextEdit):