Improve editor auto-replace (#2132)
This commit is contained in:
+184
-205
@@ -36,7 +36,6 @@ import logging
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from time import time
|
from time import time
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
||||||
@@ -85,24 +84,16 @@ class _SelectAction(Enum):
|
|||||||
MOVE_AFTER = 3
|
MOVE_AFTER = 3
|
||||||
|
|
||||||
|
|
||||||
class AutoReplaceConfig(NamedTuple):
|
|
||||||
|
|
||||||
typPadChar: str
|
|
||||||
typSQuoteO: str
|
|
||||||
typSQuoteC: str
|
|
||||||
typDQuoteO: str
|
|
||||||
typDQuoteC: str
|
|
||||||
typRepDQuote: bool
|
|
||||||
typRepSQuote: bool
|
|
||||||
typRepDash: bool
|
|
||||||
typRepDots: bool
|
|
||||||
typPadBefore: str
|
|
||||||
typPadAfter: str
|
|
||||||
|
|
||||||
|
|
||||||
class GuiDocEditor(QPlainTextEdit):
|
class GuiDocEditor(QPlainTextEdit):
|
||||||
"""Gui Widget: Main Document Editor"""
|
"""Gui Widget: Main Document Editor"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_nwDocument", "_nwItem", "_docChanged", "_docHandle", "_vpMargin",
|
||||||
|
"_lastEdit", "_lastActive", "_lastFind", "_doReplace", "_autoReplace",
|
||||||
|
"_completer", "_qDocument", "_keyContext", "_followTag1", "_followTag2",
|
||||||
|
"_timerDoc", "_wCounterDoc", "_timerSel", "_wCounterSel",
|
||||||
|
)
|
||||||
|
|
||||||
MOVE_KEYS = (
|
MOVE_KEYS = (
|
||||||
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
|
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
|
||||||
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
|
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
|
||||||
@@ -144,20 +135,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._lastFind = None # Position of the last found search word
|
self._lastFind = None # Position of the last found search word
|
||||||
self._doReplace = False # Switch to temporarily disable auto-replace
|
self._doReplace = False # Switch to temporarily disable auto-replace
|
||||||
|
|
||||||
# Typography Cache
|
# Auto-Replace
|
||||||
self._typConf = AutoReplaceConfig(
|
self._autoReplace = TextAutoReplace()
|
||||||
typPadChar=" ",
|
|
||||||
typSQuoteO="'",
|
|
||||||
typSQuoteC="'",
|
|
||||||
typDQuoteO='"',
|
|
||||||
typDQuoteC='"',
|
|
||||||
typRepSQuote=False,
|
|
||||||
typRepDQuote=False,
|
|
||||||
typRepDash=False,
|
|
||||||
typRepDots=False,
|
|
||||||
typPadBefore="",
|
|
||||||
typPadAfter="",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Completer
|
# Completer
|
||||||
self._completer = MetaCompleter(self)
|
self._completer = MetaCompleter(self)
|
||||||
@@ -195,38 +174,38 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
# Custom Shortcuts
|
# Custom Shortcuts
|
||||||
self.keyContext = QShortcut(self)
|
self._keyContext = QShortcut(self)
|
||||||
self.keyContext.setKey("Ctrl+.")
|
self._keyContext.setKey("Ctrl+.")
|
||||||
self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
self._keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
||||||
self.keyContext.activated.connect(self._openContextFromCursor)
|
self._keyContext.activated.connect(self._openContextFromCursor)
|
||||||
|
|
||||||
self.followTag1 = QShortcut(self)
|
self._followTag1 = QShortcut(self)
|
||||||
self.followTag1.setKey("Ctrl+Return")
|
self._followTag1.setKey("Ctrl+Return")
|
||||||
self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
|
self._followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
|
||||||
self.followTag1.activated.connect(self._processTag)
|
self._followTag1.activated.connect(self._processTag)
|
||||||
|
|
||||||
self.followTag2 = QShortcut(self)
|
self._followTag2 = QShortcut(self)
|
||||||
self.followTag2.setKey("Ctrl+Enter")
|
self._followTag2.setKey("Ctrl+Enter")
|
||||||
self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
|
self._followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
|
||||||
self.followTag2.activated.connect(self._processTag)
|
self._followTag2.activated.connect(self._processTag)
|
||||||
|
|
||||||
# Set Up Document Word Counter
|
# Set Up Document Word Counter
|
||||||
self.timerDoc = QTimer(self)
|
self._timerDoc = QTimer(self)
|
||||||
self.timerDoc.timeout.connect(self._runDocumentTasks)
|
self._timerDoc.timeout.connect(self._runDocumentTasks)
|
||||||
self.timerDoc.setInterval(5000)
|
self._timerDoc.setInterval(5000)
|
||||||
|
|
||||||
self.wCounterDoc = BackgroundWordCounter(self)
|
self._wCounterDoc = BackgroundWordCounter(self)
|
||||||
self.wCounterDoc.setAutoDelete(False)
|
self._wCounterDoc.setAutoDelete(False)
|
||||||
self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
|
self._wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
|
||||||
|
|
||||||
# Set Up Selection Word Counter
|
# Set Up Selection Word Counter
|
||||||
self.timerSel = QTimer(self)
|
self._timerSel = QTimer(self)
|
||||||
self.timerSel.timeout.connect(self._runSelCounter)
|
self._timerSel.timeout.connect(self._runSelCounter)
|
||||||
self.timerSel.setInterval(500)
|
self._timerSel.setInterval(500)
|
||||||
|
|
||||||
self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
|
self._wCounterSel = BackgroundWordCounter(self, forSelection=True)
|
||||||
self.wCounterSel.setAutoDelete(False)
|
self._wCounterSel.setAutoDelete(False)
|
||||||
self.wCounterSel.signals.countsReady.connect(self._updateSelCounts)
|
self._wCounterSel.signals.countsReady.connect(self._updateSelCounts)
|
||||||
|
|
||||||
# Install Event Filter for Mouse Wheel
|
# Install Event Filter for Mouse Wheel
|
||||||
self.wheelEventFilter = WheelEventFilter(self)
|
self.wheelEventFilter = WheelEventFilter(self)
|
||||||
@@ -280,8 +259,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._nwDocument = None
|
self._nwDocument = None
|
||||||
self.setReadOnly(True)
|
self.setReadOnly(True)
|
||||||
self.clear()
|
self.clear()
|
||||||
self.timerDoc.stop()
|
self._timerDoc.stop()
|
||||||
self.timerSel.stop()
|
self._timerSel.stop()
|
||||||
|
|
||||||
self._docHandle = None
|
self._docHandle = None
|
||||||
self._lastEdit = 0.0
|
self._lastEdit = 0.0
|
||||||
@@ -329,20 +308,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
settings. This function is both called when the editor is
|
settings. This function is both called when the editor is
|
||||||
created, and when the user changes the main editor preferences.
|
created, and when the user changes the main editor preferences.
|
||||||
"""
|
"""
|
||||||
# Typography
|
# Auto-Replace
|
||||||
self._typConf = AutoReplaceConfig(
|
self._autoReplace.initSettings()
|
||||||
typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP,
|
|
||||||
typSQuoteO=CONFIG.fmtSQuoteOpen,
|
|
||||||
typSQuoteC=CONFIG.fmtSQuoteClose,
|
|
||||||
typDQuoteO=CONFIG.fmtDQuoteOpen,
|
|
||||||
typDQuoteC=CONFIG.fmtDQuoteClose,
|
|
||||||
typRepSQuote=CONFIG.doReplaceSQuote,
|
|
||||||
typRepDQuote=CONFIG.doReplaceDQuote,
|
|
||||||
typRepDash=CONFIG.doReplaceDash,
|
|
||||||
typRepDots=CONFIG.doReplaceDots,
|
|
||||||
typPadBefore=CONFIG.fmtPadBefore,
|
|
||||||
typPadAfter=CONFIG.fmtPadAfter,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reload spell check and dictionaries
|
# Reload spell check and dictionaries
|
||||||
SHARED.updateSpellCheckLanguage()
|
SHARED.updateSpellCheckLanguage()
|
||||||
@@ -435,7 +402,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._lastEdit = time()
|
self._lastEdit = time()
|
||||||
self._lastActive = time()
|
self._lastActive = time()
|
||||||
self._runDocumentTasks()
|
self._runDocumentTasks()
|
||||||
self.timerDoc.start()
|
self._timerDoc.start()
|
||||||
|
|
||||||
self.setReadOnly(False)
|
self.setReadOnly(False)
|
||||||
self.updateDocMargins()
|
self.updateDocMargins()
|
||||||
@@ -757,7 +724,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
logger.debug("Requesting action: %s", action.name)
|
logger.debug("Requesting action: %s", action.name)
|
||||||
|
|
||||||
tConf = self._typConf
|
|
||||||
self._allowAutoReplace(False)
|
self._allowAutoReplace(False)
|
||||||
if action == nwDocAction.UNDO:
|
if action == nwDocAction.UNDO:
|
||||||
self.undo()
|
self.undo()
|
||||||
@@ -776,9 +742,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
elif action == nwDocAction.MD_STRIKE:
|
elif action == nwDocAction.MD_STRIKE:
|
||||||
self._toggleFormat(2, "~")
|
self._toggleFormat(2, "~")
|
||||||
elif action == nwDocAction.S_QUOTE:
|
elif action == nwDocAction.S_QUOTE:
|
||||||
self._wrapSelection(tConf.typSQuoteO, tConf.typSQuoteC)
|
self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
||||||
elif action == nwDocAction.D_QUOTE:
|
elif action == nwDocAction.D_QUOTE:
|
||||||
self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC)
|
self._wrapSelection(CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
||||||
elif action == nwDocAction.SEL_ALL:
|
elif action == nwDocAction.SEL_ALL:
|
||||||
self._makeSelection(QTextCursor.SelectionType.Document)
|
self._makeSelection(QTextCursor.SelectionType.Document)
|
||||||
elif action == nwDocAction.SEL_PARA:
|
elif action == nwDocAction.SEL_PARA:
|
||||||
@@ -804,9 +770,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
elif action == nwDocAction.BLOCK_HSC:
|
elif action == nwDocAction.BLOCK_HSC:
|
||||||
self._formatBlock(nwDocAction.BLOCK_HSC)
|
self._formatBlock(nwDocAction.BLOCK_HSC)
|
||||||
elif action == nwDocAction.REPL_SNG:
|
elif action == nwDocAction.REPL_SNG:
|
||||||
self._replaceQuotes("'", tConf.typSQuoteO, tConf.typSQuoteC)
|
self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
||||||
elif action == nwDocAction.REPL_DBL:
|
elif action == nwDocAction.REPL_DBL:
|
||||||
self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC)
|
self._replaceQuotes("\"", CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
||||||
elif action == nwDocAction.RM_BREAKS:
|
elif action == nwDocAction.RM_BREAKS:
|
||||||
self._removeInParLineBreaks()
|
self._removeInParLineBreaks()
|
||||||
elif action == nwDocAction.ALIGN_L:
|
elif action == nwDocAction.ALIGN_L:
|
||||||
@@ -878,13 +844,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
text = insert
|
text = insert
|
||||||
elif isinstance(insert, nwDocInsert):
|
elif isinstance(insert, nwDocInsert):
|
||||||
if insert == nwDocInsert.QUOTE_LS:
|
if insert == nwDocInsert.QUOTE_LS:
|
||||||
text = self._typConf.typSQuoteO
|
text = CONFIG.fmtSQuoteOpen
|
||||||
elif insert == nwDocInsert.QUOTE_RS:
|
elif insert == nwDocInsert.QUOTE_RS:
|
||||||
text = self._typConf.typSQuoteC
|
text = CONFIG.fmtSQuoteClose
|
||||||
elif insert == nwDocInsert.QUOTE_LD:
|
elif insert == nwDocInsert.QUOTE_LD:
|
||||||
text = self._typConf.typDQuoteO
|
text = CONFIG.fmtDQuoteOpen
|
||||||
elif insert == nwDocInsert.QUOTE_RD:
|
elif insert == nwDocInsert.QUOTE_RD:
|
||||||
text = self._typConf.typDQuoteC
|
text = CONFIG.fmtDQuoteClose
|
||||||
elif insert == nwDocInsert.SYNOPSIS:
|
elif insert == nwDocInsert.SYNOPSIS:
|
||||||
text = "%Synopsis: "
|
text = "%Synopsis: "
|
||||||
block = True
|
block = True
|
||||||
@@ -1120,8 +1086,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if not self._docChanged:
|
if not self._docChanged:
|
||||||
self.setDocumentChanged(removed != 0 or added != 0)
|
self.setDocumentChanged(removed != 0 or added != 0)
|
||||||
|
|
||||||
if not self.timerDoc.isActive():
|
if not self._timerDoc.isActive():
|
||||||
self.timerDoc.start()
|
self._timerDoc.start()
|
||||||
|
|
||||||
if (block := self._qDocument.findBlock(pos)).isValid():
|
if (block := self._qDocument.findBlock(pos)).isValid():
|
||||||
text = block.text()
|
text = block.text()
|
||||||
@@ -1139,7 +1105,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self._completer.setVisible(False)
|
self._completer.setVisible(False)
|
||||||
|
|
||||||
if self._doReplace and added == 1:
|
if self._doReplace and added == 1:
|
||||||
self._docAutoReplace(text)
|
cursor = self.textCursor()
|
||||||
|
if self._autoReplace.process(text, cursor):
|
||||||
|
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1261,8 +1229,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
if time() - self._lastEdit < 25.0:
|
if time() - self._lastEdit < 25.0:
|
||||||
logger.debug("Running document tasks")
|
logger.debug("Running document tasks")
|
||||||
if not self.wCounterDoc.isRunning():
|
if not self._wCounterDoc.isRunning():
|
||||||
SHARED.runInThreadPool(self.wCounterDoc)
|
SHARED.runInThreadPool(self._wCounterDoc)
|
||||||
|
|
||||||
self.docHeader.setOutline({
|
self.docHeader.setOutline({
|
||||||
block.blockNumber(): block.text()
|
block.blockNumber(): block.text()
|
||||||
@@ -1294,10 +1262,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
information to the footer, and start the selection word counter.
|
information to the footer, and start the selection word counter.
|
||||||
"""
|
"""
|
||||||
if self.textCursor().hasSelection():
|
if self.textCursor().hasSelection():
|
||||||
if not self.timerSel.isActive():
|
if not self._timerSel.isActive():
|
||||||
self.timerSel.start()
|
self._timerSel.start()
|
||||||
else:
|
else:
|
||||||
self.timerSel.stop()
|
self._timerSel.stop()
|
||||||
self.docFooter.updateWordCount(0, False)
|
self.docFooter.updateWordCount(0, False)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1307,11 +1275,11 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if self._docHandle is None:
|
if self._docHandle is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.wCounterSel.isRunning():
|
if self._wCounterSel.isRunning():
|
||||||
logger.debug("Selection word counter is busy")
|
logger.debug("Selection word counter is busy")
|
||||||
return
|
return
|
||||||
|
|
||||||
SHARED.runInThreadPool(self.wCounterSel)
|
SHARED.runInThreadPool(self._wCounterSel)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1321,7 +1289,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if self._docHandle and self._nwItem:
|
if self._docHandle and self._nwItem:
|
||||||
logger.debug("User selected %d words", wCount)
|
logger.debug("User selected %d words", wCount)
|
||||||
self.docFooter.updateWordCount(wCount, True)
|
self.docFooter.updateWordCount(wCount, True)
|
||||||
self.timerSel.stop()
|
self._timerSel.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@@ -2018,120 +1986,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _docAutoReplace(self, text: str) -> None:
|
|
||||||
"""Auto-replace text elements based on main configuration."""
|
|
||||||
cursor = self.textCursor()
|
|
||||||
tPos = cursor.positionInBlock()
|
|
||||||
tLen = len(text)
|
|
||||||
|
|
||||||
if tLen < 1 or tPos-1 > tLen:
|
|
||||||
return
|
|
||||||
|
|
||||||
t1 = text[tPos-1:tPos]
|
|
||||||
t2 = text[tPos-2:tPos]
|
|
||||||
t3 = text[tPos-3:tPos]
|
|
||||||
t4 = text[tPos-4:tPos]
|
|
||||||
|
|
||||||
if not t1:
|
|
||||||
return
|
|
||||||
|
|
||||||
delete = 0
|
|
||||||
insert = t1
|
|
||||||
tConf = self._typConf
|
|
||||||
|
|
||||||
if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'):
|
|
||||||
delete = 1
|
|
||||||
insert = tConf.typDQuoteO
|
|
||||||
|
|
||||||
elif tConf.typRepDQuote and t1 == '"':
|
|
||||||
delete = 1
|
|
||||||
if tPos == 1:
|
|
||||||
insert = tConf.typDQuoteO
|
|
||||||
elif tPos == 2 and t2 == '>"':
|
|
||||||
insert = tConf.typDQuoteO
|
|
||||||
elif tPos == 3 and t3 == '>>"':
|
|
||||||
insert = tConf.typDQuoteO
|
|
||||||
else:
|
|
||||||
insert = tConf.typDQuoteC
|
|
||||||
|
|
||||||
elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"):
|
|
||||||
delete = 1
|
|
||||||
insert = tConf.typSQuoteO
|
|
||||||
|
|
||||||
elif tConf.typRepSQuote and t1 == "'":
|
|
||||||
delete = 1
|
|
||||||
if tPos == 1:
|
|
||||||
insert = tConf.typSQuoteO
|
|
||||||
elif tPos == 2 and t2 == ">'":
|
|
||||||
insert = tConf.typSQuoteO
|
|
||||||
elif tPos == 3 and t3 == ">>'":
|
|
||||||
insert = tConf.typSQuoteO
|
|
||||||
else:
|
|
||||||
insert = tConf.typSQuoteC
|
|
||||||
|
|
||||||
elif tConf.typRepDash and t4 == "----":
|
|
||||||
delete = 4
|
|
||||||
insert = nwUnicode.U_HBAR
|
|
||||||
|
|
||||||
elif tConf.typRepDash and t3 == "---":
|
|
||||||
delete = 3
|
|
||||||
insert = nwUnicode.U_EMDASH
|
|
||||||
|
|
||||||
elif tConf.typRepDash and t2 == "--":
|
|
||||||
delete = 2
|
|
||||||
insert = nwUnicode.U_ENDASH
|
|
||||||
|
|
||||||
elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-":
|
|
||||||
delete = 2
|
|
||||||
insert = nwUnicode.U_EMDASH
|
|
||||||
|
|
||||||
elif tConf.typRepDash and t2 == nwUnicode.U_EMDASH + "-":
|
|
||||||
delete = 2
|
|
||||||
insert = nwUnicode.U_HBAR
|
|
||||||
|
|
||||||
elif tConf.typRepDots and t3 == "...":
|
|
||||||
delete = 3
|
|
||||||
insert = nwUnicode.U_HELLIP
|
|
||||||
|
|
||||||
elif t1 == nwUnicode.U_LSEP:
|
|
||||||
# This resolves issue #1150
|
|
||||||
delete = 1
|
|
||||||
insert = nwUnicode.U_PSEP
|
|
||||||
|
|
||||||
check = insert
|
|
||||||
if tConf.typPadBefore and check in tConf.typPadBefore:
|
|
||||||
if self._allowSpaceBeforeColon(text, check):
|
|
||||||
delete = max(delete, 1)
|
|
||||||
chkPos = tPos - delete - 1
|
|
||||||
if chkPos >= 0 and text[chkPos].isspace():
|
|
||||||
# Strip existing space before inserting a new (#1061)
|
|
||||||
delete += 1
|
|
||||||
insert = tConf.typPadChar + insert
|
|
||||||
|
|
||||||
if tConf.typPadAfter and check in tConf.typPadAfter:
|
|
||||||
if self._allowSpaceBeforeColon(text, check):
|
|
||||||
delete = max(delete, 1)
|
|
||||||
insert = insert + tConf.typPadChar
|
|
||||||
|
|
||||||
if delete > 0:
|
|
||||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
|
||||||
cursor.insertText(insert)
|
|
||||||
|
|
||||||
# Re-highlight, since the auto-replace sometimes interferes with it
|
|
||||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _allowSpaceBeforeColon(text: str, char: str) -> bool:
|
|
||||||
"""Special checker function only used by the insert space
|
|
||||||
feature for French, Spanish, etc, so it doesn't insert a
|
|
||||||
space before colons in meta data lines. See issue #1090.
|
|
||||||
"""
|
|
||||||
if char == ":" and len(text) > 1 and text[0] == "@":
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
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. The selection will be the
|
on user settings and document action. The selection will be the
|
||||||
@@ -2340,6 +2194,131 @@ class BackgroundWordCounterSignals(QObject):
|
|||||||
countsReady = pyqtSignal(int, int, int)
|
countsReady = pyqtSignal(int, int, int)
|
||||||
|
|
||||||
|
|
||||||
|
class TextAutoReplace:
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC",
|
||||||
|
"_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots",
|
||||||
|
"_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.initSettings()
|
||||||
|
return
|
||||||
|
|
||||||
|
def initSettings(self) -> None:
|
||||||
|
"""Initialise the auto-replace settings from config."""
|
||||||
|
self._quoteSO = CONFIG.fmtSQuoteOpen
|
||||||
|
self._quoteSC = CONFIG.fmtSQuoteClose
|
||||||
|
self._quoteDO = CONFIG.fmtDQuoteOpen
|
||||||
|
self._quoteDC = CONFIG.fmtDQuoteClose
|
||||||
|
|
||||||
|
self._replaceSQuote = CONFIG.doReplaceSQuote
|
||||||
|
self._replaceDQuote = CONFIG.doReplaceDQuote
|
||||||
|
self._replaceDash = CONFIG.doReplaceDash
|
||||||
|
self._replaceDots = CONFIG.doReplaceDots
|
||||||
|
|
||||||
|
self._padChar = nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP
|
||||||
|
self._padBefore = CONFIG.fmtPadBefore
|
||||||
|
self._padAfter = CONFIG.fmtPadAfter
|
||||||
|
self._doPadBefore = bool(CONFIG.fmtPadBefore)
|
||||||
|
self._doPadAfter = bool(CONFIG.fmtPadAfter)
|
||||||
|
return
|
||||||
|
|
||||||
|
def process(self, text: str, cursor: QTextCursor) -> bool:
|
||||||
|
"""Auto-replace text elements based on main configuration.
|
||||||
|
Returns True if anything was changed.
|
||||||
|
"""
|
||||||
|
pos = cursor.positionInBlock()
|
||||||
|
length = len(text)
|
||||||
|
if length < 1 or pos-1 > length:
|
||||||
|
return False
|
||||||
|
|
||||||
|
delete, insert = self._determine(text, pos)
|
||||||
|
if insert == "":
|
||||||
|
return False
|
||||||
|
|
||||||
|
check = insert
|
||||||
|
if self._doPadBefore and check in self._padBefore:
|
||||||
|
if not (check == ":" and length > 1 and text[0] == "@"):
|
||||||
|
delete = max(delete, 1)
|
||||||
|
chkPos = pos - delete - 1
|
||||||
|
if chkPos >= 0 and text[chkPos].isspace():
|
||||||
|
# Strip existing space before inserting a new (#1061)
|
||||||
|
delete += 1
|
||||||
|
insert = self._padChar + insert
|
||||||
|
|
||||||
|
if self._doPadAfter and check in self._padAfter:
|
||||||
|
if not (check == ":" and length > 1 and text[0] == "@"):
|
||||||
|
delete = max(delete, 1)
|
||||||
|
insert = insert + self._padChar
|
||||||
|
|
||||||
|
if delete > 0:
|
||||||
|
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
||||||
|
cursor.insertText(insert)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _determine(self, text: str, pos: int) -> tuple[int, str]:
|
||||||
|
"""Determine what to replace, if anything."""
|
||||||
|
t1 = text[pos-1:pos]
|
||||||
|
t2 = text[pos-2:pos]
|
||||||
|
t3 = text[pos-3:pos]
|
||||||
|
t4 = text[pos-4:pos]
|
||||||
|
if t1 == "":
|
||||||
|
# Return early if there is nothing to check
|
||||||
|
return 0, ""
|
||||||
|
|
||||||
|
leading = t2[:1].isspace()
|
||||||
|
if self._replaceDQuote:
|
||||||
|
if leading and t2.endswith('"'):
|
||||||
|
return 1, self._quoteDO
|
||||||
|
elif t1 == '"':
|
||||||
|
if pos == 1:
|
||||||
|
return 1, self._quoteDO
|
||||||
|
elif pos == 2 and t2 == '>"':
|
||||||
|
return 1, self._quoteDO
|
||||||
|
elif pos == 3 and t3 == '>>"':
|
||||||
|
return 1, self._quoteDO
|
||||||
|
else:
|
||||||
|
return 1, self._quoteDC
|
||||||
|
|
||||||
|
if self._replaceSQuote:
|
||||||
|
if leading and t2.endswith("'"):
|
||||||
|
return 1, self._quoteSO
|
||||||
|
elif t1 == "'":
|
||||||
|
if pos == 1:
|
||||||
|
return 1, self._quoteSO
|
||||||
|
elif pos == 2 and t2 == ">'":
|
||||||
|
return 1, self._quoteSO
|
||||||
|
elif pos == 3 and t3 == ">>'":
|
||||||
|
return 1, self._quoteSO
|
||||||
|
else:
|
||||||
|
return 1, self._quoteSC
|
||||||
|
|
||||||
|
if self._replaceDash:
|
||||||
|
if t4 == "----":
|
||||||
|
return 4, "\u2015" # Horizontal bar
|
||||||
|
elif t3 == "---":
|
||||||
|
return 3, "\u2014" # Long dash
|
||||||
|
elif t2 == "--":
|
||||||
|
return 2, "\u2013" # Short dash
|
||||||
|
elif t2 == "\u2013-":
|
||||||
|
return 2, "\u2014" # Long dash
|
||||||
|
elif t2 == "\u2014-":
|
||||||
|
return 2, "\u2015" # Horizontal bar
|
||||||
|
|
||||||
|
if self._replaceDots and t3 == "...":
|
||||||
|
return 3, "\u2026" # Ellipsis
|
||||||
|
|
||||||
|
if t1 == "\u2028": # Line separator
|
||||||
|
# This resolves issue #1150
|
||||||
|
return 1, "\u2029" # Paragraph separator
|
||||||
|
|
||||||
|
return 0, t1
|
||||||
|
|
||||||
|
|
||||||
class GuiDocToolBar(QWidget):
|
class GuiDocToolBar(QWidget):
|
||||||
"""The Formatting and Options Fold Out Menu
|
"""The Formatting and Options Fold Out Menu
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
|||||||
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
|
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
|
||||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
|
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
|
||||||
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
|
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
|
||||||
assert docEditor._typConf.typPadChar == nwUnicode.U_NBSP
|
assert docEditor._autoReplace._padChar == nwUnicode.U_NBSP
|
||||||
assert docEditor.docHeader.itemTitle.text() == (
|
assert docEditor.docHeader.itemTitle.text() == (
|
||||||
"Novel \u203a New Folder \u203a New Scene"
|
"Novel \u203a New Folder \u203a New Scene"
|
||||||
)
|
)
|
||||||
@@ -106,7 +106,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
|||||||
assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators
|
assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators
|
||||||
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
|
||||||
assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff
|
assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff
|
||||||
assert docEditor._typConf.typPadChar == nwUnicode.U_THNBSP
|
assert docEditor._autoReplace._padChar == nwUnicode.U_THNBSP
|
||||||
assert docEditor.docHeader.itemTitle.text() == "New Scene"
|
assert docEditor.docHeader.itemTitle.text() == "New Scene"
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
@@ -1917,8 +1917,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
|
|
||||||
threadPool = MockThreadPool()
|
threadPool = MockThreadPool()
|
||||||
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
|
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
|
||||||
docEditor.timerDoc.blockSignals(True)
|
docEditor._timerDoc.blockSignals(True)
|
||||||
docEditor.timerSel.blockSignals(True)
|
docEditor._timerSel.blockSignals(True)
|
||||||
|
|
||||||
buildTestProject(nwGUI, projPath)
|
buildTestProject(nwGUI, projPath)
|
||||||
|
|
||||||
@@ -1944,20 +1944,20 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
|
|
||||||
# Check that a busy counter is blocked
|
# Check that a busy counter is blocked
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(docEditor.wCounterDoc, "isRunning", lambda *a: True)
|
mp.setattr(docEditor._wCounterDoc, "isRunning", lambda *a: True)
|
||||||
docEditor._runDocumentTasks()
|
docEditor._runDocumentTasks()
|
||||||
assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
||||||
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(docEditor.wCounterSel, "isRunning", lambda *a: True)
|
mp.setattr(docEditor._wCounterSel, "isRunning", lambda *a: True)
|
||||||
docEditor._runSelCounter()
|
docEditor._runSelCounter()
|
||||||
assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
||||||
|
|
||||||
# Run the full word counter
|
# Run the full word counter
|
||||||
docEditor._runDocumentTasks()
|
docEditor._runDocumentTasks()
|
||||||
assert threadPool.objectID() == id(docEditor.wCounterDoc)
|
assert threadPool.objectID() == id(docEditor._wCounterDoc)
|
||||||
|
|
||||||
docEditor.wCounterDoc.run()
|
docEditor._wCounterDoc.run()
|
||||||
# docEditor._updateDocCounts(cC, wC, pC)
|
# docEditor._updateDocCounts(cC, wC, pC)
|
||||||
assert SHARED.project.tree[C.hSceneDoc]._charCount == cC # type: ignore
|
assert SHARED.project.tree[C.hSceneDoc]._charCount == cC # type: ignore
|
||||||
assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC # type: ignore
|
assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC # type: ignore
|
||||||
@@ -1967,9 +1967,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
|||||||
# Select all text and run the selection word counter
|
# Select all text and run the selection word counter
|
||||||
docEditor.docAction(nwDocAction.SEL_ALL)
|
docEditor.docAction(nwDocAction.SEL_ALL)
|
||||||
docEditor._runSelCounter()
|
docEditor._runSelCounter()
|
||||||
assert threadPool.objectID() == id(docEditor.wCounterSel)
|
assert threadPool.objectID() == id(docEditor._wCounterSel)
|
||||||
|
|
||||||
docEditor.wCounterSel.run()
|
docEditor._wCounterSel.run()
|
||||||
assert docEditor.docFooter.wordsText.text() == f"Words: {wC} selected"
|
assert docEditor.docFooter.wordsText.text() == f"Words: {wC} selected"
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
||||||
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
|
||||||
|
|
||||||
docEditor.wCounterDoc.run()
|
docEditor._wCounterDoc.run()
|
||||||
|
|
||||||
# Spell Checking
|
# Spell Checking
|
||||||
# ==============
|
# ==============
|
||||||
|
|||||||
Reference in New Issue
Block a user