Make some improvements to the doc editor code

This commit is contained in:
Veronica Berglyd Olsen
2025-01-22 00:35:01 +01:00
parent 3672ae8be6
commit 5b840ada83
+50 -80
View File
@@ -219,7 +219,7 @@ class GuiDocEditor(QPlainTextEdit):
self.changeFocusState = self.docHeader.changeFocusState self.changeFocusState = self.docHeader.changeFocusState
# Finalise # Finalise
self.updateSyntaxColours() self.updateSyntaxColors()
self.initEditor() self.initEditor()
logger.debug("Ready: GuiDocEditor") logger.debug("Ready: GuiDocEditor")
@@ -287,7 +287,7 @@ class GuiDocEditor(QPlainTextEdit):
self.docToolBar.updateTheme() self.docToolBar.updateTheme()
return return
def updateSyntaxColours(self) -> None: def updateSyntaxColors(self) -> None:
"""Update the syntax highlighting theme.""" """Update the syntax highlighting theme."""
syntax = SHARED.theme.syntaxTheme syntax = SHARED.theme.syntaxTheme
@@ -304,7 +304,7 @@ class GuiDocEditor(QPlainTextEdit):
viewport.setPalette(palette) viewport.setPalette(palette)
self.docHeader.matchColours() self.docHeader.matchColours()
self.docFooter.matchColours() self.docFooter.matchColors()
return return
@@ -339,14 +339,12 @@ class GuiDocEditor(QPlainTextEdit):
# Also set the document text options for the document text flow # Also set the document text options for the document text flow
options = QTextOption() options = QTextOption()
if CONFIG.doJustify: if CONFIG.doJustify:
options.setAlignment(QtAlignJustify) options.setAlignment(QtAlignJustify)
if CONFIG.showTabsNSpaces: if CONFIG.showTabsNSpaces:
options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces) options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces)
if CONFIG.showLineEndings: if CONFIG.showLineEndings:
options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators) options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators)
self._qDocument.setDefaultTextOption(options) self._qDocument.setDefaultTextOption(options)
# Scrolling # Scrolling
@@ -379,19 +377,19 @@ class GuiDocEditor(QPlainTextEdit):
"""Load text from a document into the editor. If we have an I/O """Load text from a document into the editor. If we have an I/O
error, we must handle this and clear the editor so that we don't error, we must handle this and clear the editor so that we don't
risk overwriting the file if it exists. This can for instance risk overwriting the file if it exists. This can for instance
happen of the file contains binary elements or an encoding that happen if the file contains binary elements or an encoding that
novelWriter does not support. If loading is successful, or the novelWriter does not support. If loading is successful, or the
document is new (empty string), we set up the editor for editing document is new (empty string), we set up the editor for editing
the file. the file.
""" """
self._nwDocument = SHARED.project.storage.getDocument(tHandle) self._nwDocument = SHARED.project.storage.getDocument(tHandle)
self._nwItem = self._nwDocument.nwItem self._nwItem = self._nwDocument.nwItem
if not ((nwItem := self._nwItem) and nwItem.itemType == nwItemType.FILE): if not (self._nwItem and self._nwItem.itemType == nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle) logger.debug("Requested item '%s' is not a document", tHandle)
self.clearEditor() self.clearEditor()
return False return False
if (docText := self._nwDocument.readDocument()) is None: if (text := self._nwDocument.readDocument()) is None:
# There was an I/O error # There was an I/O error
self.clearEditor() self.clearEditor()
return False return False
@@ -400,7 +398,7 @@ class GuiDocEditor(QPlainTextEdit):
self._docHandle = tHandle self._docHandle = tHandle
self._allowAutoReplace(False) self._allowAutoReplace(False)
self._qDocument.setTextContent(docText, tHandle) self._qDocument.setTextContent(text, tHandle)
self._allowAutoReplace(True) self._allowAutoReplace(True)
QApplication.processEvents() QApplication.processEvents()
@@ -415,7 +413,7 @@ class GuiDocEditor(QPlainTextEdit):
if isinstance(tLine, int): if isinstance(tLine, int):
self.setCursorLine(tLine) self.setCursorLine(tLine)
else: else:
self.setCursorPosition(nwItem.cursorPos) self.setCursorPosition(self._nwItem.cursorPos)
self.docHeader.setHandle(tHandle) self.docHeader.setHandle(tHandle)
self.docFooter.setHandle(tHandle) self.docFooter.setHandle(tHandle)
@@ -438,7 +436,7 @@ class GuiDocEditor(QPlainTextEdit):
# Finalise # Finalise
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
self.updateStatusMessage.emit( self.updateStatusMessage.emit(
self.tr("Opened Document: {0}").format(nwItem.itemName) self.tr("Opened Document: {0}").format(self._nwItem.itemName)
) )
return True return True
@@ -469,19 +467,17 @@ class GuiDocEditor(QPlainTextEdit):
) )
return False return False
docText = self.getText() text = self.getText()
cC, wC, pC = standardCounter(docText) cC, wC, pC = standardCounter(text)
self._updateDocCounts(cC, wC, pC) self._updateDocCounts(cC, wC, pC)
if not self._nwDocument.writeDocument(docText): if not self._nwDocument.writeDocument(text):
saveOk = False saveOk = False
if self._nwDocument.hashError: if self._nwDocument.hashError and SHARED.question(self.tr(
msgYes = SHARED.question(self.tr( "This document has been changed outside of novelWriter "
"This document has been changed outside of novelWriter " "while it was open. Overwrite the file on disk?"
"while it was open. Overwrite the file on disk?" )):
)) saveOk = self._nwDocument.writeDocument(text, forceWrite=True)
if msgYes:
saveOk = self._nwDocument.writeDocument(docText, forceWrite=True)
if not saveOk: if not saveOk:
SHARED.error( SHARED.error(
@@ -494,10 +490,8 @@ class GuiDocEditor(QPlainTextEdit):
self.setDocumentChanged(False) self.setDocumentChanged(False)
self.docTextChanged.emit(self._docHandle, self._lastEdit) self.docTextChanged.emit(self._docHandle, self._lastEdit)
oldHeader = self._nwItem.mainHeading
oldCount = SHARED.project.index.getHandleHeaderCount(tHandle) oldCount = SHARED.project.index.getHandleHeaderCount(tHandle)
SHARED.project.index.scanText(tHandle, docText) SHARED.project.index.scanText(tHandle, text)
newHeader = self._nwItem.mainHeading
newCount = SHARED.project.index.getHandleHeaderCount(tHandle) newCount = SHARED.project.index.getHandleHeaderCount(tHandle)
if self._nwItem.itemClass == nwItemClass.NOVEL: if self._nwItem.itemClass == nwItemClass.NOVEL:
@@ -506,9 +500,6 @@ class GuiDocEditor(QPlainTextEdit):
else: else:
self.novelStructureChanged.emit() self.novelStructureChanged.emit()
if oldHeader != newHeader:
self.docFooter.updateInfo()
# Update the status bar # Update the status bar
self.updateStatusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName)) self.updateStatusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
@@ -589,7 +580,7 @@ class GuiDocEditor(QPlainTextEdit):
QTextDocument->toRawText instead of toPlainText. The former preserves QTextDocument->toRawText instead of toPlainText. The former preserves
non-breaking spaces, the latter does not. We still want to get rid of non-breaking spaces, the latter does not. We still want to get rid of
paragraph and line separators though. paragraph and line separators though.
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText
""" """
text = self._qDocument.toRawText() text = self._qDocument.toRawText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
@@ -627,10 +618,9 @@ class GuiDocEditor(QPlainTextEdit):
def setCursorPosition(self, position: int) -> None: def setCursorPosition(self, position: int) -> None:
"""Move the cursor to a given position in the document.""" """Move the cursor to a given position in the document."""
nChars = self._qDocument.characterCount() if (chars := self._qDocument.characterCount()) > 1 and isinstance(position, int):
if nChars > 1 and isinstance(position, int):
cursor = self.textCursor() cursor = self.textCursor()
cursor.setPosition(minmax(position, 0, nChars-1)) cursor.setPosition(minmax(position, 0, chars-1))
self.setTextCursor(cursor) self.setTextCursor(cursor)
self.centerCursor() self.centerCursor()
return return
@@ -693,12 +683,10 @@ class GuiDocEditor(QPlainTextEdit):
def spellCheckDocument(self) -> None: def spellCheckDocument(self) -> None:
"""Rerun the highlighter to update spell checking status of the """Rerun the highlighter to update spell checking status of the
currently loaded text. The fastest way to do this, at least as currently loaded text.
of Qt 5.13, is to clear the text and put it back. This clears
the undo stack, so we only do it for big documents.
""" """
logger.debug("Running spell checker")
start = time() start = time()
logger.debug("Running spell checker")
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self._qDocument.syntaxHighlighter.rehighlight() self._qDocument.syntaxHighlighter.rehighlight()
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
@@ -939,9 +927,7 @@ class GuiDocEditor(QPlainTextEdit):
* We also handle automatic scrolling here. * We also handle automatic scrolling here.
""" """
self._lastActive = time() self._lastActive = time()
isReturn = event.key() == Qt.Key.Key_Return if self.docSearch.anyFocus() and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
isReturn |= event.key() == Qt.Key.Key_Enter
if isReturn and self.docSearch.anyFocus():
return return
elif event == QKeySequence.StandardKey.Redo: elif event == QKeySequence.StandardKey.Redo:
self.docAction(nwDocAction.REDO) self.docAction(nwDocAction.REDO)
@@ -1094,7 +1080,7 @@ class GuiDocEditor(QPlainTextEdit):
if (block := self._qDocument.findBlock(pos)).isValid(): if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text() text = block.text()
if text.startswith("@") and added + removed == 1: if text.startswith("@") and added + removed == 1:
# Only run on single keypresses, otherwise it will trigger # Only run on single character changes, or it will trigger
# at unwanted times when other changes are made to the document # at unwanted times when other changes are made to the document
cursor = self.textCursor() cursor = self.textCursor()
bPos = cursor.positionInBlock() bPos = cursor.positionInBlock()
@@ -1106,10 +1092,10 @@ class GuiDocEditor(QPlainTextEdit):
else: else:
self._completer.setVisible(False) self._completer.setVisible(False)
if self._doReplace and added == 1: if self._doReplace and added == 1:
cursor = self.textCursor() cursor = self.textCursor()
if self._autoReplace.process(text, cursor): if self._autoReplace.process(text, cursor):
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block()) self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return return
@@ -1123,11 +1109,10 @@ class GuiDocEditor(QPlainTextEdit):
def _insertCompletion(self, pos: int, length: int, text: str) -> None: def _insertCompletion(self, pos: int, length: int, text: str) -> None:
"""Insert choice from the completer menu.""" """Insert choice from the completer menu."""
cursor = self.textCursor() cursor = self.textCursor()
block = cursor.block() if (block := cursor.block()).isValid():
if block.isValid(): check = pos + block.position()
pos += block.position() cursor.setPosition(check, QtMoveAnchor)
cursor.setPosition(pos, QtMoveAnchor) cursor.setPosition(check + length, QtKeepAnchor)
cursor.setPosition(pos + length, QtKeepAnchor)
cursor.insertText(text) cursor.insertText(text)
self._completer.hide() self._completer.hide()
return return
@@ -1220,7 +1205,8 @@ class GuiDocEditor(QPlainTextEdit):
# Execute the context menu # Execute the context menu
if viewport := self.viewport(): if viewport := self.viewport():
ctxMenu.exec(viewport.mapToGlobal(pos)) ctxMenu.exec(viewport.mapToGlobal(pos))
ctxMenu.deleteLater()
ctxMenu.setParent(None)
return return
@@ -1277,15 +1263,11 @@ class GuiDocEditor(QPlainTextEdit):
@pyqtSlot() @pyqtSlot()
def _runSelCounter(self) -> None: def _runSelCounter(self) -> None:
"""Update the selection word count.""" """Update the selection word count."""
if self._docHandle is None: if self._docHandle:
return if self._wCounterSel.isRunning():
logger.debug("Selection word counter is busy")
if self._wCounterSel.isRunning(): return
logger.debug("Selection word counter is busy") SHARED.runInThreadPool(self._wCounterSel)
return
SHARED.runInThreadPool(self._wCounterSel)
return return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
@@ -1565,8 +1547,10 @@ class GuiDocEditor(QPlainTextEdit):
return return
def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None, def _wrapSelection(
select: _SelectAction = _SelectAction.NO_DECISION) -> None: self, before: str, after: str | None = None, pos: int | None = None,
select: _SelectAction = _SelectAction.NO_DECISION
) -> None:
"""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
@@ -1928,8 +1912,9 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument.syntaxHighlighter.rehighlightBlock(block) self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return return
def _processTag(self, cursor: QTextCursor | None = None, def _processTag(
follow: bool = True, create: bool = False) -> nwTrinary: self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
) -> nwTrinary:
"""Activated by Ctrl+Enter. Checks that we're in a block """Activated by Ctrl+Enter. Checks that we're in a block
starting with '@'. We then find the tag under the cursor and starting with '@'. We then find the tag under the cursor and
check that it is not the tag itself. If all this is fine, we check that it is not the tag itself. If all this is fine, we
@@ -2163,13 +2148,10 @@ class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor: GuiDocEditor, forSelection: bool = False) -> None: def __init__(self, docEditor: GuiDocEditor, forSelection: bool = False) -> None:
super().__init__() super().__init__()
self._docEditor = docEditor self._docEditor = docEditor
self._forSelection = forSelection self._forSelection = forSelection
self._isRunning = False self._isRunning = False
self.signals = BackgroundWordCounterSignals() self.signals = BackgroundWordCounterSignals()
return return
def isRunning(self) -> bool: def isRunning(self) -> bool:
@@ -3079,42 +3061,30 @@ class GuiDocEditFooter(QWidget):
# Status # Status
self.statusIcon = QLabel("", self) self.statusIcon = QLabel("", self)
self.statusIcon.setContentsMargins(0, 0, 0, 0)
self.statusIcon.setFixedHeight(iPx) self.statusIcon.setFixedHeight(iPx)
self.statusIcon.setAlignment(QtAlignLeftTop) self.statusIcon.setAlignment(QtAlignLeftTop)
self.statusText = QLabel(self.tr("Status"), self) self.statusText = QLabel("", self)
self.statusText.setIndent(0)
self.statusText.setMargin(0)
self.statusText.setContentsMargins(0, 0, 0, 0)
self.statusText.setAutoFillBackground(True) self.statusText.setAutoFillBackground(True)
self.statusText.setFixedHeight(fPx) self.statusText.setFixedHeight(fPx)
self.statusText.setAlignment(QtAlignLeftTop) self.statusText.setAlignment(QtAlignLeftTop)
# Lines # Lines
self.linesIcon = QLabel("", self) self.linesIcon = QLabel("", self)
self.linesIcon.setContentsMargins(0, 0, 0, 0)
self.linesIcon.setFixedHeight(iPx) self.linesIcon.setFixedHeight(iPx)
self.linesIcon.setAlignment(QtAlignLeftTop) self.linesIcon.setAlignment(QtAlignLeftTop)
self.linesText = QLabel("", self) self.linesText = QLabel("", self)
self.linesText.setIndent(0)
self.linesText.setMargin(0)
self.linesText.setContentsMargins(0, 0, 0, 0)
self.linesText.setAutoFillBackground(True) self.linesText.setAutoFillBackground(True)
self.linesText.setFixedHeight(fPx) self.linesText.setFixedHeight(fPx)
self.linesText.setAlignment(QtAlignLeftTop) self.linesText.setAlignment(QtAlignLeftTop)
# Words # Words
self.wordsIcon = QLabel("", self) self.wordsIcon = QLabel("", self)
self.wordsIcon.setContentsMargins(0, 0, 0, 0)
self.wordsIcon.setFixedHeight(iPx) self.wordsIcon.setFixedHeight(iPx)
self.wordsIcon.setAlignment(QtAlignLeftTop) self.wordsIcon.setAlignment(QtAlignLeftTop)
self.wordsText = QLabel("", self) self.wordsText = QLabel("", self)
self.wordsText.setIndent(0)
self.wordsText.setMargin(0)
self.wordsText.setContentsMargins(0, 0, 0, 0)
self.wordsText.setAutoFillBackground(True) self.wordsText.setAutoFillBackground(True)
self.wordsText.setFixedHeight(fPx) self.wordsText.setFixedHeight(fPx)
self.wordsText.setAlignment(QtAlignLeftTop) self.wordsText.setAlignment(QtAlignLeftTop)
@@ -3167,10 +3137,10 @@ class GuiDocEditFooter(QWidget):
iPx = round(0.9*SHARED.theme.baseIconHeight) iPx = round(0.9*SHARED.theme.baseIconHeight)
self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx))) self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx))) self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
self.matchColours() self.matchColors()
return return
def matchColours(self) -> None: def matchColors(self) -> None:
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
""" """