Improve how formatting is applied in the editor (#1600)
This commit is contained in:
@@ -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
|
||||||
# =================
|
# =================
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user