Improve how formatting is applied in the editor (#1600)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-12 20:45:37 +01:00
committed by GitHub
3 changed files with 97 additions and 97 deletions
+1 -2
View File
@@ -327,8 +327,7 @@ class nwQuotes:
class nwUnicode: class nwUnicode:
"""Supported unicode character constants and their HTML equivalents. """Supported unicode character constants and their HTML equivalents."""
"""
# Unicode Constants # Unicode Constants
# ================= # =================
+89 -52
View File
@@ -67,6 +67,16 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class _SelectAction(Enum):
NO_DECISION = 0
KEEP_SELECTION = 1
KEEP_POSITION = 2
MOVE_AFTER = 3
# END Class _SelectAction
class GuiDocEditor(QPlainTextEdit): class GuiDocEditor(QPlainTextEdit):
"""Gui Widget: Main Document Editor""" """Gui Widget: Main Document Editor"""
@@ -1410,17 +1420,28 @@ class GuiDocEditor(QPlainTextEdit):
If more than one block is selected, the formatting is applied to If more than one block is selected, the formatting is applied to
the first block. the first block.
""" """
cursor = self._autoSelect() cursor = self.textCursor()
if not cursor.hasSelection(): posO = cursor.position()
logger.warning("No selection made, nothing to do") if cursor.hasSelection():
return False 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() posS = cursor.selectionStart()
posE = cursor.selectionEnd() 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) blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE) blockE = self._qDocument.findBlock(posE)
if blockS != blockE: if blockS != blockE:
posE = blockS.position() + blockS.length() - 1 posE = blockS.position() + blockS.length() - 1
cursor.clearSelection() cursor.clearSelection()
@@ -1443,34 +1464,26 @@ class GuiDocEditor(QPlainTextEdit):
break break
if fLen == min(numA, numB): 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: else:
self._wrapSelection(fChar*fLen) self._wrapSelection(fChar*fLen, pos=posO, select=select)
return True return True
def _clearSurrounding(self, cursor: QTextCursor, nChars: int) -> bool: def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None,
"""Clear n characters before and after the cursor.""" select: _SelectAction = _SelectAction.NO_DECISION) -> bool:
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:
"""Wrap the selected text in whatever is in tBefore and tAfter. """Wrap the selected text in whatever is in tBefore and tAfter.
If there is no selection, the autoSelect setting decides the If there is no selection, the autoSelect setting decides the
action. AutoSelect will select the word under the cursor before action. AutoSelect will select the word under the cursor before
@@ -1479,10 +1492,17 @@ class GuiDocEditor(QPlainTextEdit):
if after is None: if after is None:
after = before after = before
cursor = self._autoSelect() cursor = self.textCursor()
if not cursor.hasSelection(): posO = pos if isinstance(pos, int) else cursor.position()
logger.warning("No selection made, nothing to do") if select == _SelectAction.NO_DECISION:
return False 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() posS = cursor.selectionStart()
posE = cursor.selectionEnd() posE = cursor.selectionEnd()
@@ -1500,8 +1520,14 @@ class GuiDocEditor(QPlainTextEdit):
cursor.insertText(before) cursor.insertText(before)
cursor.endEditBlock() cursor.endEditBlock()
cursor.setPosition(posE + len(before), QTextCursor.MoveAnchor) if select == _SelectAction.MOVE_AFTER:
cursor.setPosition(posS + len(before), QTextCursor.KeepAnchor) 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) self.setTextCursor(cursor)
return True return True
@@ -1908,27 +1934,38 @@ class GuiDocEditor(QPlainTextEdit):
def _autoSelect(self) -> QTextCursor: def _autoSelect(self) -> QTextCursor:
"""Return a cursor which may or may not have a selection based """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() cursor = self.textCursor()
if CONFIG.autoSelect and not cursor.hasSelection(): if CONFIG.autoSelect and not cursor.hasSelection():
cursor.select(QTextCursor.WordUnderCursor) cPos = cursor.position()
posS = cursor.selectionStart() bPos = cursor.block().position()
posE = cursor.selectionEnd() bLen = cursor.block().length()
# Underscore counts as a part of the word, so check that the # Scan backwards
# selection isn't wrapped in italics markers. sPos = cPos
reSelect = False for i in range(cPos - bPos):
if self._qDocument.characterAt(posS) == "_": sPos = cPos - i - 1
posS += 1 if not self._qDocument.characterAt(sPos).isalnum():
reSelect = True sPos += 1
if self._qDocument.characterAt(posE) == "_": break
posE -= 1
reSelect = True # Scan forwards
if reSelect: ePos = cPos
cursor.clearSelection() for i in range(bPos + bLen - cPos):
cursor.setPosition(posS, QTextCursor.MoveAnchor) ePos = cPos + i
cursor.setPosition(posE-1, QTextCursor.KeepAnchor) if not self._qDocument.characterAt(ePos).isalnum():
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) self.setTextCursor(cursor)
+7 -43
View File
@@ -672,7 +672,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd
@pytest.mark.gui @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.""" """Test the text manipulation functions."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True 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) text = "### A Scene\n\n%s" % "\n\n".join(ipsumText)
nwGUI.docEditor.replaceText(text) 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 # Wrap Selection
# ============== # ==============
@@ -718,11 +687,6 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex
nwGUI.docEditor.replaceText(text) nwGUI.docEditor.replaceText(text)
nwGUI.docEditor.setCursorPosition(45) 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 # Wrap Equal
nwGUI.docEditor.replaceText(text) nwGUI.docEditor.replaceText(text)
nwGUI.docEditor.setCursorPosition(45) 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]) text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2])
nwGUI.docEditor.replaceText(text)
nwGUI.docEditor.setCursorPosition(45)
# No Selection # Block format repetition
with monkeypatch.context() as mp: nwGUI.docEditor.replaceText(text)
mp.setattr(nwGUI.docEditor, "_autoSelect", lambda: QTextCursor()) nwGUI.docEditor.setCursorPosition(39)
assert nwGUI.docEditor._toggleFormat(2, "=") is False 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 # Wrap Single Equal
nwGUI.docEditor.replaceText(text) nwGUI.docEditor.replaceText(text)