-
From e210bca31977c0f8550b151ede596b90a7a19dbe Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 10 Sep 2023 17:46:35 +0200
Subject: [PATCH 02/15] Move the thread pool to the shared object instance
---
novelwriter/gui/doceditor.py | 4 ++--
novelwriter/gui/editordocument.py | 9 +++++++++
novelwriter/guimain.py | 3 +--
novelwriter/shared.py | 16 +++++++++++++++-
tests/test_gui/test_gui_doceditor.py | 9 +++++----
5 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index bc526608..9b4cd827 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1139,7 +1139,7 @@ class GuiDocEditor(QPlainTextEdit):
if time() - self._lastEdit < 5 * self.wcInterval:
logger.debug("Running word counter")
- self.mainGui.threadPool.start(self.wCounterDoc)
+ SHARED.runInThreadPool(self.wCounterDoc)
return
@@ -1192,7 +1192,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Selection word counter is busy")
return
- self.mainGui.threadPool.start(self.wCounterSel)
+ SHARED.runInThreadPool(self.wCounterSel)
return
diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py
index 40b15288..434f3914 100644
--- a/novelwriter/gui/editordocument.py
+++ b/novelwriter/gui/editordocument.py
@@ -51,10 +51,19 @@ class GuiTextDocument(QTextDocument):
logger.debug("Delete: GuiTextDocument")
return
+ ##
+ # Properties
+ ##
+
@property
def syntaxHighlighter(self) -> GuiDocHighlighter:
+ """Return the document's syntax highlighter object."""
return self._syntax
+ ##
+ # Metods
+ ##
+
def setTextContent(self, text: str, tHandle: str) -> None:
"""Set the text content of the document."""
self._syntax.setHandle(tHandle)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index feafec69..b19f585f 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -30,7 +30,7 @@ from time import time
from pathlib import Path
from datetime import datetime
-from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
+from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import (
QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
@@ -95,7 +95,6 @@ class GuiMain(QMainWindow):
logger.debug("Create: GUI")
self.setObjectName("GuiMain")
- self.threadPool = QThreadPool(self)
# System Info
# ===========
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 2a186a75..b5e6d4fd 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -29,7 +29,7 @@ from time import time
from typing import TYPE_CHECKING
from pathlib import Path
-from PyQt5.QtCore import QObject, pyqtSignal
+from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
from PyQt5.QtWidgets import QMessageBox, QWidget
from novelwriter.core.spellcheck import NWSpellEnchant
@@ -55,14 +55,23 @@ class SharedData(QObject):
def __init__(self) -> None:
super().__init__()
+
+ # Objects
self._gui = None
self._theme = None
self._project = None
self._spelling = None
+
+ # Settings
self._lockedBy = None
self._alert = None
self._idleTime = 0.0
self._idleRefTime = time()
+
+ # Threading
+ self._threadPool = QThreadPool(self)
+ self._threadPool.setMaxThreadCount(5)
+
return
##
@@ -199,6 +208,11 @@ class SharedData(QObject):
self.projectStatusChanged.emit(state)
return
+ def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
+ """Queue a runnable in the application thread pool."""
+ self._threadPool.start(runnable, priority=priority)
+ return
+
##
# Alert Boxes
##
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 8bee4ab4..276bafa5 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -1100,13 +1100,14 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
def __init__(self):
self._objID = None
- def start(self, runObj):
+ def start(self, runObj, priority=0):
self._objID = id(runObj)
def objectID(self):
return self._objID
- nwGUI.threadPool = MockThreadPool()
+ threadPool = MockThreadPool()
+ monkeypatch.setattr(SHARED, "_threadPool", threadPool)
nwGUI.docEditor.wcTimerDoc.blockSignals(True)
nwGUI.docEditor.wcTimerSel.blockSignals(True)
@@ -1145,7 +1146,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
# Run the full word counter
nwGUI.docEditor._runDocCounter()
- assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
+ assert SHARED._threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
nwGUI.docEditor.wCounterDoc.run()
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
@@ -1161,7 +1162,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
# Run the selection word counter
nwGUI.docEditor._runSelCounter()
- assert nwGUI.threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
+ assert SHARED._threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
nwGUI.docEditor.wCounterSel.run()
# nwGUI.docEditor._updateSelCounts(cC, wC, pC)
From 3f91f5c50633c62174265f3abe3cef60f46c2179 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 10 Sep 2023 18:15:16 +0200
Subject: [PATCH 03/15] Use the global thread pool, like the docs suggest
---
novelwriter/__init__.py | 2 +-
novelwriter/shared.py | 9 +++------
tests/test_gui/test_gui_doceditor.py | 8 ++++----
3 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 3b49359a..b3b39028 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -73,7 +73,7 @@ logger = logging.getLogger(__name__)
# Main Program
##
-# Global config singleton
+# Global config and data singletons
CONFIG = Config()
SHARED = SharedData()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index b5e6d4fd..8ee3099a 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -68,10 +68,6 @@ class SharedData(QObject):
self._idleTime = 0.0
self._idleRefTime = time()
- # Threading
- self._threadPool = QThreadPool(self)
- self._threadPool.setMaxThreadCount(5)
-
return
##
@@ -138,7 +134,8 @@ class SharedData(QObject):
self._gui = gui
self._theme = theme
self._resetProject()
- logger.debug("SharedData instance initialised")
+ logger.debug("Ready: SharedData")
+ logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
return
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
@@ -210,7 +207,7 @@ class SharedData(QObject):
def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
"""Queue a runnable in the application thread pool."""
- self._threadPool.start(runnable, priority=priority)
+ QThreadPool.globalInstance().start(runnable, priority=priority)
return
##
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 276bafa5..faa75027 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -24,7 +24,7 @@ import pytest
from mocked import causeOSError
from tools import C, buildTestProject
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import QThreadPool, Qt
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
from PyQt5.QtWidgets import QAction, qApp
@@ -1107,7 +1107,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
return self._objID
threadPool = MockThreadPool()
- monkeypatch.setattr(SHARED, "_threadPool", threadPool)
+ monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
nwGUI.docEditor.wcTimerDoc.blockSignals(True)
nwGUI.docEditor.wcTimerSel.blockSignals(True)
@@ -1146,7 +1146,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
# Run the full word counter
nwGUI.docEditor._runDocCounter()
- assert SHARED._threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
+ assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
nwGUI.docEditor.wCounterDoc.run()
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
@@ -1162,7 +1162,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
# Run the selection word counter
nwGUI.docEditor._runSelCounter()
- assert SHARED._threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
+ assert threadPool.objectID() == id(nwGUI.docEditor.wCounterSel)
nwGUI.docEditor.wCounterSel.run()
# nwGUI.docEditor._updateSelCounts(cC, wC, pC)
From f1e28e21f887a795d4ab5b55e5624bc8d2bffb8b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 10 Sep 2023 21:27:34 +0200
Subject: [PATCH 04/15] Cache spell check errors for usage in the editor when
correcting
---
novelwriter/gui/doceditor.py | 158 +++++++++++-------------------
novelwriter/gui/dochighlight.py | 68 ++++++++-----
novelwriter/gui/editordocument.py | 52 +++++++++-
3 files changed, 153 insertions(+), 125 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 9b4cd827..dc9b93af 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -318,16 +318,16 @@ class GuiDocEditor(QPlainTextEdit):
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
# Also set the document text options for the document text flow
- theOpt = QTextOption()
+ options = QTextOption()
if CONFIG.doJustify:
- theOpt.setAlignment(Qt.AlignJustify)
+ options.setAlignment(Qt.AlignJustify)
if CONFIG.showTabsNSpaces:
- theOpt.setFlags(theOpt.flags() | QTextOption.ShowTabsAndSpaces)
+ options.setFlags(options.flags() | QTextOption.ShowTabsAndSpaces)
if CONFIG.showLineEndings:
- theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
+ options.setFlags(options.flags() | QTextOption.ShowLineAndParagraphSeparators)
- self._qDocument.setDefaultTextOption(theOpt)
+ self._qDocument.setDefaultTextOption(options)
# Scroll bars
if CONFIG.hideVScroll:
@@ -378,11 +378,9 @@ class GuiDocEditor(QPlainTextEdit):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._docHandle = tHandle
- tStart = time()
self._allowAutoReplace(False)
self._qDocument.setTextContent(docText, tHandle)
self._allowAutoReplace(True)
- logger.debug("Document text set in %.3f ms", 1000*(time() - tStart))
qApp.processEvents()
self._lastEdit = time()
@@ -1003,100 +1001,63 @@ class GuiDocEditor(QPlainTextEdit):
"""Triggered by right click to open the context menu. Also
triggered by the Ctrl+. shortcut.
"""
- userCursor = self.textCursor()
- userSelection = userCursor.hasSelection()
- posCursor = self.cursorForPosition(pos)
+ uCursor = self.textCursor()
+ pCursor = self.cursorForPosition(pos)
- mnuContext = QMenu()
+ ctxMenu = QMenu()
- # Follow, Cut, Copy and Paste
- # ===========================
+ # Follow
+ if self._followTag(cursor=pCursor, loadTag=False):
+ aTag = ctxMenu.addAction(self.tr("Follow Tag"))
+ aTag.triggered.connect(lambda: self._followTag(cursor=pCursor))
+ ctxMenu.addSeparator()
- if self._followTag(cursor=posCursor, loadTag=False):
- mnuTag = QAction(self.tr("Follow Tag"), mnuContext)
- mnuTag.triggered.connect(lambda: self._followTag(cursor=posCursor))
- mnuContext.addAction(mnuTag)
- mnuContext.addSeparator()
+ # Cut, Copy and Paste
+ if uCursor.hasSelection():
+ aCut = ctxMenu.addAction(self.tr("Cut"))
+ aCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
+ aCopy = ctxMenu.addAction(self.tr("Copy"))
+ aCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
- if userSelection:
- mnuCut = QAction(self.tr("Cut"), mnuContext)
- mnuCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
- mnuContext.addAction(mnuCut)
-
- mnuCopy = QAction(self.tr("Copy"), mnuContext)
- mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
- mnuContext.addAction(mnuCopy)
-
- mnuPaste = QAction(self.tr("Paste"), mnuContext)
- mnuPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
- mnuContext.addAction(mnuPaste)
-
- mnuContext.addSeparator()
+ aPaste = ctxMenu.addAction(self.tr("Paste"))
+ aPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
+ ctxMenu.addSeparator()
# Selections
- # ==========
-
- mnuSelAll = QAction(self.tr("Select All"), mnuContext)
- mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
- mnuContext.addAction(mnuSelAll)
-
- mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
- mnuSelWord.triggered.connect(
- lambda: self._makePosSelection(QTextCursor.WordUnderCursor, pos)
- )
- mnuContext.addAction(mnuSelWord)
-
- mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
- mnuSelPara.triggered.connect(
- lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, pos)
- )
- mnuContext.addAction(mnuSelPara)
+ aSAll = ctxMenu.addAction(self.tr("Select All"))
+ aSAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
+ aSWrd = ctxMenu.addAction(self.tr("Select Word"))
+ aSWrd.triggered.connect(lambda: self._makePosSelection(QTextCursor.WordUnderCursor, pos))
+ aSPar = ctxMenu.addAction(self.tr("Select Paragraph"))
+ aSPar.triggered.connect(lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, pos))
# Spell Checking
- # ==============
+ if self._spellCheck:
+ word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position())
+ if word and cPos >= 0 and cLen > 0:
+ logger.debug("Word '%s' is misspelled", word)
+ block = pCursor.block()
+ sCursor = self.textCursor()
+ sCursor.setPosition(block.position() + cPos)
+ sCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, cLen)
+ if suggest:
+ ctxMenu.addSeparator()
+ ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
+ for option in suggest:
+ aFix = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
+ aFix.triggered.connect(
+ lambda _, option=option: self._correctWord(sCursor, option)
+ )
+ else:
+ ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")))
- posCursor = self.cursorForPosition(pos)
- spellCheck = self._spellCheck
- theWord = ""
+ ctxMenu.addSeparator()
+ aAdd = QAction(self.tr("Add Word to Dictionary"), ctxMenu)
+ aAdd.triggered.connect(lambda: self._addWord(word, block))
+ ctxMenu.addAction(aAdd)
- if posCursor.block().text().startswith("@"):
- spellCheck = False
-
- if spellCheck:
- posCursor.select(QTextCursor.WordUnderCursor)
- theWord = posCursor.selectedText().strip().strip(self._nonWord)
- spellCheck &= theWord != ""
-
- if spellCheck:
- logger.debug("Looking up '%s' in the dictionary", theWord)
- spellCheck &= not SHARED.spelling.checkWord(theWord)
-
- if spellCheck:
- mnuContext.addSeparator()
- mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
- mnuContext.addAction(mnuHead)
-
- theSuggest = SHARED.spelling.suggestWords(theWord)[:15]
- if len(theSuggest) > 0:
- for aWord in theSuggest:
- mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
- mnuWord.triggered.connect(
- lambda thePos, aWord=aWord: self._correctWord(posCursor, aWord)
- )
- mnuContext.addAction(mnuWord)
- else:
- mnuHead = QAction(
- "%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")), mnuContext
- )
- mnuContext.addAction(mnuHead)
-
- mnuContext.addSeparator()
- mnuAdd = QAction(self.tr("Add Word to Dictionary"), mnuContext)
- mnuAdd.triggered.connect(lambda thePos: self._addWord(posCursor))
- mnuContext.addAction(mnuAdd)
-
- # Open the context menu
- mnuContext.exec_(self.viewport().mapToGlobal(pos))
+ # Execute the context menu
+ ctxMenu.exec_(self.viewport().mapToGlobal(pos))
return
@@ -1105,24 +1066,23 @@ class GuiDocEditor(QPlainTextEdit):
"""Slot for the spell check context menu triggering the
replacement of a word with the word from the dictionary.
"""
- xPos = cursor.selectionStart()
+ pos = cursor.selectionStart()
cursor.beginEditBlock()
cursor.removeSelectedText()
cursor.insertText(word)
cursor.endEditBlock()
- cursor.setPosition(xPos)
+ cursor.setPosition(pos)
self.setTextCursor(cursor)
return
- @pyqtSlot("QTextCursor")
- def _addWord(self, cursor: QTextCursor) -> None:
+ @pyqtSlot(str, "QTextBlock")
+ def _addWord(self, word: str, block: QTextBlock) -> None:
"""Slot for the spell check context menu triggered when the user
wants to add a word to the project dictionary.
"""
- theWord = cursor.selectedText().strip().strip(self._nonWord)
- logger.debug("Added '%s' to project dictionary", theWord)
- SHARED.spelling.addWord(theWord)
- self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
+ logger.debug("Added '%s' to project dictionary", word)
+ SHARED.spelling.addWord(word)
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return
@pyqtSlot()
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 92b9d8ca..9bc8d953 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -29,7 +29,8 @@ from time import time
from PyQt5.QtCore import Qt, QRegularExpression
from PyQt5.QtGui import (
- QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush, QTextDocument
+ QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData,
+ QTextCharFormat, QTextDocument
)
from novelwriter import CONFIG, SHARED
@@ -38,6 +39,9 @@ from novelwriter.constants import nwRegEx, nwUnicode
logger = logging.getLogger(__name__)
+SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—]+\b")
+SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
+
class GuiDocHighlighter(QSyntaxHighlighter):
@@ -54,7 +58,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._tItem = None
self._tHandle = None
self._spellCheck = False
- self._spellRx = QRegularExpression()
self._hRules: list[tuple[str, dict]] = []
self._hStyles: dict[str, QTextCharFormat] = {}
@@ -223,13 +226,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
self.rxRules.append((hReg, regRules))
- # Build a QRegExp for the spell checker
- # Include additional characters that the highlighter should
- # consider to be word separators
- uCode = nwUnicode.U_ENDASH + nwUnicode.U_EMDASH
- self._spellRx = QRegularExpression(r"\b[^\s\-\+\/" + uCode + r"]+\b")
- self._spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
-
return
##
@@ -383,19 +379,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if not self._spellCheck:
return
- rxSpell = self._spellRx.globalMatch(text.replace("_", " "), 0)
- while rxSpell.hasNext():
- rxMatch = rxSpell.next()
- if not SHARED.spelling.checkWord(rxMatch.captured(0)):
- if not rxMatch.captured(0).isalpha() or rxMatch.captured(0).isupper():
- continue
- xPos = rxMatch.capturedStart(0)
- xLen = rxMatch.capturedLength(0)
- for x in range(xPos, xPos+xLen):
- spFmt = self.format(x)
- spFmt.setUnderlineColor(self._colSpell)
- spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
- self.setFormat(x, 1, spFmt)
+ data = self.currentBlockUserData()
+ if not isinstance(data, TextBlockData):
+ data = TextBlockData()
+ self.setCurrentBlockUserData(data)
+
+ for xPos, xLen in data.spellCheck(text):
+ for x in range(xPos, xPos+xLen):
+ spFmt = self.format(x)
+ spFmt.setUnderlineColor(self._colSpell)
+ spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ self.setFormat(x, 1, spFmt)
return
@@ -435,3 +429,33 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return charFormat
# END Class GuiDocHighlighter
+
+
+class TextBlockData(QTextBlockUserData):
+
+ __slots__ = ("_spellErrors")
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._spellErrors: list[tuple[int, int]] = []
+ return
+
+ @property
+ def spellErrors(self) -> list[tuple[int, int]]:
+ """Return spell error data from last check."""
+ return self._spellErrors
+
+ def spellCheck(self, text: str) -> list[tuple[int, int]]:
+ """Run the spell checker and cache the result, and return the
+ list of spell check errors.
+ """
+ self._spellErrors = []
+ rxSpell = SPELLRX.globalMatch(text.replace("_", " "), 0)
+ while rxSpell.hasNext():
+ rxMatch = rxSpell.next()
+ if not SHARED.spelling.checkWord(rxMatch.captured(0)):
+ if rxMatch.captured(0).isalpha() and not rxMatch.captured(0).isupper():
+ self._spellErrors.append((rxMatch.capturedStart(0), rxMatch.capturedLength(0)))
+ return self._spellErrors
+
+# END Class TextBlockData
diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py
index 434f3914..14d7f4fd 100644
--- a/novelwriter/gui/editordocument.py
+++ b/novelwriter/gui/editordocument.py
@@ -24,12 +24,15 @@ along with this program. If not, see .
from __future__ import annotations
import logging
+
+from time import time
+
+from PyQt5.QtGui import QTextCursor, QTextDocument
from PyQt5.QtCore import QObject
+from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp
+from novelwriter import SHARED
-from PyQt5.QtGui import QTextDocument
-from PyQt5.QtWidgets import QPlainTextDocumentLayout
-
-from novelwriter.gui.dochighlight import GuiDocHighlighter
+from novelwriter.gui.dochighlight import GuiDocHighlighter, TextBlockData
logger = logging.getLogger(__name__)
@@ -67,7 +70,48 @@ class GuiTextDocument(QTextDocument):
def setTextContent(self, text: str, tHandle: str) -> None:
"""Set the text content of the document."""
self._syntax.setHandle(tHandle)
+
+ self.blockSignals(True)
+ self.setUndoRedoEnabled(False)
+ self.clear()
+
+ tStart = time()
+
self.setPlainText(text)
+ count = self.lineCount()
+
+ tMid = time()
+
+ self.setUndoRedoEnabled(True)
+ self.blockSignals(False)
+ self._syntax.rehighlight()
+ qApp.processEvents()
+
+ tEnd = time()
+
+ logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart))
+ logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid))
+
return
+ def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]:
+ """Check if there is a misspelled word at a given position in
+ the document, and if so, return it.
+ """
+ cursor = QTextCursor(self)
+ cursor.setPosition(pos)
+ block = cursor.block()
+ if block.isValid():
+ data = block.userData()
+ if isinstance(data, TextBlockData):
+ text = block.text()
+ check = pos - block.position()
+ if check >= 0:
+ for cPos, cLen in data.spellErrors:
+ cEnd = cPos + cLen
+ if cPos <= check <= cEnd:
+ word = text[cPos:cEnd]
+ return word, cPos, cLen, SHARED.spelling.suggestWords(word)
+ return "", -1, -1, []
+
# END Class GuiTextDocument
From dea5c54ef04e665b4eb8e1a0c2fba4531fbe24a7 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 10 Sep 2023 21:52:38 +0200
Subject: [PATCH 05/15] Make some minor improvements to spell check suggest
---
novelwriter/gui/doceditor.py | 5 ++---
novelwriter/gui/editordocument.py | 21 ++++++++++-----------
2 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index dc9b93af..bf343578 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1043,7 +1043,7 @@ class GuiDocEditor(QPlainTextEdit):
if suggest:
ctxMenu.addSeparator()
ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
- for option in suggest:
+ for option in suggest[:15]:
aFix = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
aFix.triggered.connect(
lambda _, option=option: self._correctWord(sCursor, option)
@@ -1052,9 +1052,8 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")))
ctxMenu.addSeparator()
- aAdd = QAction(self.tr("Add Word to Dictionary"), ctxMenu)
+ aAdd = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
aAdd.triggered.connect(lambda: self._addWord(word, block))
- ctxMenu.addAction(aAdd)
# Execute the context menu
ctxMenu.exec_(self.viewport().mapToGlobal(pos))
diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py
index 14d7f4fd..793d82bc 100644
--- a/novelwriter/gui/editordocument.py
+++ b/novelwriter/gui/editordocument.py
@@ -101,17 +101,16 @@ class GuiTextDocument(QTextDocument):
cursor = QTextCursor(self)
cursor.setPosition(pos)
block = cursor.block()
- if block.isValid():
- data = block.userData()
- if isinstance(data, TextBlockData):
- text = block.text()
- check = pos - block.position()
- if check >= 0:
- for cPos, cLen in data.spellErrors:
- cEnd = cPos + cLen
- if cPos <= check <= cEnd:
- word = text[cPos:cEnd]
- return word, cPos, cLen, SHARED.spelling.suggestWords(word)
+ data = block.userData()
+ if block.isValid() and isinstance(data, TextBlockData):
+ text = block.text()
+ check = pos - block.position()
+ if check >= 0:
+ for cPos, cLen in data.spellErrors:
+ cEnd = cPos + cLen
+ if cPos <= check <= cEnd:
+ word = text[cPos:cEnd]
+ return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, []
# END Class GuiTextDocument
From 45a4503fa38a0d7fcc9c05c63436bd4d24e6bc44 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 10 Sep 2023 23:43:50 +0200
Subject: [PATCH 06/15] Clean up variables in document editor
---
novelwriter/gui/doceditor.py | 568 +++++++++++++++++------------------
1 file changed, 282 insertions(+), 286 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index bf343578..eedbca6f 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -616,8 +616,8 @@ class GuiDocEditor(QPlainTextEdit):
def toggleSpellCheck(self, state: bool | None) -> None:
"""This is the main spell check setting function, and this one
should call all other setSpellCheck functions in other classes.
- If the spell check mode (theMode) is not defined (None), then
- toggle the current status saved in this class.
+ If the spell check state is not defined (None), then toggle the
+ current status saved in this class.
"""
if state is None:
state = not self._spellCheck
@@ -784,30 +784,30 @@ class GuiDocEditor(QPlainTextEdit):
goAfter = False
if isinstance(insert, str):
- theText = insert
+ text = insert
elif isinstance(insert, nwDocInsert):
if insert == nwDocInsert.QUOTE_LS:
- theText = self._typSQuoteO
+ text = self._typSQuoteO
elif insert == nwDocInsert.QUOTE_RS:
- theText = self._typSQuoteC
+ text = self._typSQuoteC
elif insert == nwDocInsert.QUOTE_LD:
- theText = self._typDQuoteO
+ text = self._typDQuoteO
elif insert == nwDocInsert.QUOTE_RD:
- theText = self._typDQuoteC
+ text = self._typDQuoteC
elif insert == nwDocInsert.SYNOPSIS:
- theText = "% Synopsis: "
+ text = "% Synopsis: "
newBlock = True
goAfter = True
elif insert == nwDocInsert.NEW_PAGE:
- theText = "[NEW PAGE]"
+ text = "[NEW PAGE]"
newBlock = True
goAfter = False
elif insert == nwDocInsert.VSPACE_S:
- theText = "[VSPACE]"
+ text = "[VSPACE]"
newBlock = True
goAfter = False
elif insert == nwDocInsert.VSPACE_M:
- theText = "[VSPACE:2]"
+ text = "[VSPACE:2]"
newBlock = True
goAfter = False
else:
@@ -816,42 +816,42 @@ class GuiDocEditor(QPlainTextEdit):
return False
if newBlock:
- self.insertNewBlock(theText, defaultAfter=goAfter)
+ self.insertNewBlock(text, defaultAfter=goAfter)
else:
- theCursor = self.textCursor()
- theCursor.beginEditBlock()
- theCursor.insertText(theText)
- theCursor.endEditBlock()
+ cursor = self.textCursor()
+ cursor.beginEditBlock()
+ cursor.insertText(text)
+ cursor.endEditBlock()
return True
def insertNewBlock(self, text: str, defaultAfter: bool = True) -> bool:
"""Insert a piece of text on a blank line."""
- theCursor = self.textCursor()
- theBlock = theCursor.block()
- if not theBlock.isValid():
+ cursor = self.textCursor()
+ block = cursor.block()
+ if not block.isValid():
logger.error("Not a valid text block")
return False
- sPos = theBlock.position()
- sLen = theBlock.length()
+ sPos = block.position()
+ sLen = block.length()
- theCursor.beginEditBlock()
+ cursor.beginEditBlock()
if sLen > 1 and defaultAfter:
- theCursor.setPosition(sPos + sLen - 1)
- theCursor.insertText("\n")
+ cursor.setPosition(sPos + sLen - 1)
+ cursor.insertText("\n")
else:
- theCursor.setPosition(sPos)
+ cursor.setPosition(sPos)
- theCursor.insertText(text)
+ cursor.insertText(text)
if sLen > 1 and not defaultAfter:
- theCursor.insertText("\n")
+ cursor.insertText("\n")
- theCursor.endEditBlock()
+ cursor.endEditBlock()
- self.setTextCursor(theCursor)
+ self.setTextCursor(cursor)
return True
@@ -944,8 +944,7 @@ class GuiDocEditor(QPlainTextEdit):
follow tag function.
"""
if qApp.keyboardModifiers() == Qt.ControlModifier:
- theCursor = self.cursorForPosition(event.pos())
- self._followTag(theCursor)
+ self._followTag(self.cursorForPosition(event.pos()))
super().mouseReleaseEvent(event)
self.docFooter.updateLineCount()
return
@@ -1173,9 +1172,9 @@ class GuiDocEditor(QPlainTextEdit):
def beginSearch(self) -> None:
"""Set the selected text as the search text."""
- theCursor = self.textCursor()
- if theCursor.hasSelection():
- self.docSearch.setSearchText(theCursor.selectedText())
+ cursor = self.textCursor()
+ if cursor.hasSelection():
+ self.docSearch.setSearchText(cursor.selectedText())
else:
self.docSearch.setSearchText(None)
resS, _ = self.findAllOccurences()
@@ -1213,8 +1212,8 @@ class GuiDocEditor(QPlainTextEdit):
self.beginSearch()
return
- theCursor = self.textCursor()
- resIdx = bisect.bisect_left(resS, theCursor.position())
+ cursor = self.textCursor()
+ resIdx = bisect.bisect_left(resS, cursor.position())
doLoop = self.docSearch.doLoop
maxIdx = len(resS) - 1
@@ -1235,9 +1234,9 @@ class GuiDocEditor(QPlainTextEdit):
else:
resIdx = 0 if doLoop else maxIdx
- theCursor.setPosition(resS[resIdx], QTextCursor.MoveAnchor)
- theCursor.setPosition(resE[resIdx], QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ cursor.setPosition(resS[resIdx], QTextCursor.MoveAnchor)
+ cursor.setPosition(resE[resIdx], QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
self.docFooter.updateLineCount()
self.docSearch.setResultCount(resIdx + 1, len(resS))
@@ -1251,14 +1250,14 @@ class GuiDocEditor(QPlainTextEdit):
"""
resS = []
resE = []
- theCursor = self.textCursor()
- hasSelection = theCursor.hasSelection()
+ cursor = self.textCursor()
+ hasSelection = cursor.hasSelection()
if hasSelection:
- origA = theCursor.selectionStart()
- origB = theCursor.selectionEnd()
+ origA = cursor.selectionStart()
+ origB = cursor.selectionEnd()
else:
- origA = theCursor.position()
- origB = theCursor.position()
+ origA = cursor.position()
+ origB = cursor.position()
findOpt = QTextDocument.FindFlag(0)
if self.docSearch.isCaseSense:
@@ -1267,27 +1266,27 @@ class GuiDocEditor(QPlainTextEdit):
findOpt |= QTextDocument.FindWholeWords
searchFor = self.docSearch.getSearchObject()
- theCursor.setPosition(0)
- self.setTextCursor(theCursor)
+ cursor.setPosition(0)
+ self.setTextCursor(cursor)
# Search up to a maximum of 1000, and make sure certain special
# searches like a regex search for .* turns into an infinite loop
while self.find(searchFor, findOpt) and len(resE) <= 1000:
- theCursor = self.textCursor()
- if theCursor.hasSelection():
- resS.append(theCursor.selectionStart())
- resE.append(theCursor.selectionEnd())
+ cursor = self.textCursor()
+ if cursor.hasSelection():
+ resS.append(cursor.selectionStart())
+ resE.append(cursor.selectionEnd())
else:
logger.warning("The search returned an empty result")
break
if hasSelection:
- theCursor.setPosition(origA, QTextCursor.MoveAnchor)
- theCursor.setPosition(origB, QTextCursor.KeepAnchor)
+ cursor.setPosition(origA, QTextCursor.MoveAnchor)
+ cursor.setPosition(origB, QTextCursor.KeepAnchor)
else:
- theCursor.setPosition(origA)
+ cursor.setPosition(origA)
- self.setTextCursor(theCursor)
+ self.setTextCursor(cursor)
return resS, resE
@@ -1305,24 +1304,24 @@ class GuiDocEditor(QPlainTextEdit):
self.beginSearch()
return
- theCursor = self.textCursor()
- if not theCursor.hasSelection():
+ cursor = self.textCursor()
+ if not cursor.hasSelection():
# We have no text selected at all, so just make this a
# regular find next call.
self.findNext()
return
- if self._lastFind is None and theCursor.hasSelection():
+ if self._lastFind is None and cursor.hasSelection():
# If we have a selection but no search, it may have been the
# text we triggered the search with, in which case we search
# again from the beginning of that selection to make sure we
# have a valid result.
- sPos = theCursor.selectionStart()
- theCursor.clearSelection()
- theCursor.setPosition(sPos)
- self.setTextCursor(theCursor)
+ sPos = cursor.selectionStart()
+ cursor.clearSelection()
+ cursor.setPosition(sPos)
+ self.setTextCursor(cursor)
self.findNext()
- theCursor = self.textCursor()
+ cursor = self.textCursor()
if self._lastFind is None:
# In case the above didn't find a result, we give up here.
@@ -1332,26 +1331,26 @@ class GuiDocEditor(QPlainTextEdit):
replWith = self.docSearch.replaceText
if self.docSearch.doMatchCap:
- replWith = transferCase(theCursor.selectedText(), replWith)
+ replWith = transferCase(cursor.selectedText(), replWith)
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
- isFind = self._lastFind[0] == theCursor.selectionStart()
- isFind &= self._lastFind[1] == theCursor.selectionEnd()
+ isFind = self._lastFind[0] == cursor.selectionStart()
+ isFind &= self._lastFind[1] == cursor.selectionEnd()
except Exception:
isFind = False
if isFind:
- theCursor.beginEditBlock()
- theCursor.removeSelectedText()
- theCursor.insertText(replWith)
- theCursor.endEditBlock()
- theCursor.setPosition(theCursor.selectionEnd())
- self.setTextCursor(theCursor)
+ cursor.beginEditBlock()
+ cursor.removeSelectedText()
+ cursor.insertText(replWith)
+ cursor.endEditBlock()
+ cursor.setPosition(cursor.selectionEnd())
+ self.setTextCursor(cursor)
logger.debug(
"Replaced occurrence of '%s' with '%s' on line %d",
- searchFor, replWith, theCursor.blockNumber()
+ searchFor, replWith, cursor.blockNumber()
)
else:
logger.error("The selected text is not a search result, skipping replace")
@@ -1369,23 +1368,23 @@ class GuiDocEditor(QPlainTextEdit):
If more than one block is selected, the formatting is applied to
the first block.
"""
- theCursor = self._autoSelect()
- if not theCursor.hasSelection():
+ cursor = self._autoSelect()
+ if not cursor.hasSelection():
logger.warning("No selection made, nothing to do")
return False
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
- theCursor.clearSelection()
- theCursor.setPosition(posS, QTextCursor.MoveAnchor)
- theCursor.setPosition(posE, QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ cursor.clearSelection()
+ cursor.setPosition(posS, QTextCursor.MoveAnchor)
+ cursor.setPosition(posE, QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
numB = 0
for n in range(fLen):
@@ -1402,7 +1401,7 @@ class GuiDocEditor(QPlainTextEdit):
break
if fLen == min(numA, numB):
- self._clearSurrounding(theCursor, fLen)
+ self._clearSurrounding(cursor, fLen)
else:
self._wrapSelection(fChar*fLen)
@@ -1438,51 +1437,51 @@ class GuiDocEditor(QPlainTextEdit):
if after is None:
after = before
- theCursor = self._autoSelect()
- if not theCursor.hasSelection():
+ cursor = self._autoSelect()
+ if not cursor.hasSelection():
logger.warning("No selection made, nothing to do")
return False
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
- theCursor.clearSelection()
- theCursor.beginEditBlock()
- theCursor.setPosition(posE)
- theCursor.insertText(after)
- theCursor.setPosition(posS)
- theCursor.insertText(before)
- theCursor.endEditBlock()
+ cursor.clearSelection()
+ cursor.beginEditBlock()
+ cursor.setPosition(posE)
+ cursor.insertText(after)
+ cursor.setPosition(posS)
+ cursor.insertText(before)
+ cursor.endEditBlock()
- theCursor.setPosition(posE + len(before), QTextCursor.MoveAnchor)
- theCursor.setPosition(posS + len(before), QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ cursor.setPosition(posE + len(before), QTextCursor.MoveAnchor)
+ cursor.setPosition(posS + len(before), QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
return True
def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:
"""Replace all straight quotes in the selected text."""
- theCursor = self.textCursor()
- if not theCursor.hasSelection():
+ cursor = self.textCursor()
+ if not cursor.hasSelection():
SHARED.error(self.tr("Please select some text before calling replace quotes."))
return False
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
closeCheck = (
" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
)
self._allowAutoReplace(False)
for posC in range(posS, posE+1):
- theCursor.setPosition(posC)
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
- selText = theCursor.selectedText()
+ cursor.setPosition(posC)
+ cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
+ selText = cursor.selectedText()
nS = len(selText)
if nS == 2:
@@ -1497,18 +1496,18 @@ class GuiDocEditor(QPlainTextEdit):
if cC != sQuote:
continue
- theCursor.clearSelection()
- theCursor.setPosition(posC)
+ cursor.clearSelection()
+ cursor.setPosition(posC)
if pC in closeCheck:
- theCursor.beginEditBlock()
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
- theCursor.insertText(oQuote)
- theCursor.endEditBlock()
+ cursor.beginEditBlock()
+ cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
+ cursor.insertText(oQuote)
+ cursor.endEditBlock()
else:
- theCursor.beginEditBlock()
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
- theCursor.insertText(cQuote)
- theCursor.endEditBlock()
+ cursor.beginEditBlock()
+ cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
+ cursor.insertText(cQuote)
+ cursor.endEditBlock()
self._allowAutoReplace(True)
@@ -1516,133 +1515,133 @@ class GuiDocEditor(QPlainTextEdit):
def _formatBlock(self, action: nwDocAction) -> bool:
"""Change the block format of the block under the cursor."""
- theCursor = self.textCursor()
- theBlock = theCursor.block()
- if not theBlock.isValid():
+ cursor = self.textCursor()
+ block = cursor.block()
+ if not block.isValid():
logger.debug("Invalid block selected for action '%s'", str(action))
return False
# Remove existing format first, if any
- theText = theBlock.text()
- hasText = len(theText) > 0
- if theText.startswith("@"):
+ setText = block.text()
+ hasText = len(setText) > 0
+ if setText.startswith("@"):
logger.error("Cannot apply block format to keyword/value line")
return False
- elif theText.startswith("% "):
- newText = theText[2:]
+ elif setText.startswith("% "):
+ newText = setText[2:]
cOffset = 2
if action == nwDocAction.BLOCK_COM:
action = nwDocAction.BLOCK_TXT
- elif theText.startswith("%"):
- newText = theText[1:]
+ elif setText.startswith("%"):
+ newText = setText[1:]
cOffset = 1
if action == nwDocAction.BLOCK_COM:
action = nwDocAction.BLOCK_TXT
- elif theText.startswith("# "):
- newText = theText[2:]
+ elif setText.startswith("# "):
+ newText = setText[2:]
cOffset = 2
- elif theText.startswith("## "):
- newText = theText[3:]
+ elif setText.startswith("## "):
+ newText = setText[3:]
cOffset = 3
- elif theText.startswith("### "):
- newText = theText[4:]
+ elif setText.startswith("### "):
+ newText = setText[4:]
cOffset = 4
- elif theText.startswith("#### "):
- newText = theText[5:]
+ elif setText.startswith("#### "):
+ newText = setText[5:]
cOffset = 5
- elif theText.startswith("#! "):
- newText = theText[3:]
+ elif setText.startswith("#! "):
+ newText = setText[3:]
cOffset = 3
- elif theText.startswith("##! "):
- newText = theText[4:]
+ elif setText.startswith("##! "):
+ newText = setText[4:]
cOffset = 4
- elif theText.startswith(">> "):
- newText = theText[3:]
+ elif setText.startswith(">> "):
+ newText = setText[3:]
cOffset = 3
- elif theText.startswith("> ") and action != nwDocAction.INDENT_R:
- newText = theText[2:]
+ elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
+ newText = setText[2:]
cOffset = 2
- elif theText.startswith(">>"):
- newText = theText[2:]
+ elif setText.startswith(">>"):
+ newText = setText[2:]
cOffset = 2
- elif theText.startswith(">") and action != nwDocAction.INDENT_R:
- newText = theText[1:]
+ elif setText.startswith(">") and action != nwDocAction.INDENT_R:
+ newText = setText[1:]
cOffset = 1
else:
- newText = theText
+ newText = setText
cOffset = 0
# Also remove formatting tags at the end
- if theText.endswith(" <<"):
+ if setText.endswith(" <<"):
newText = newText[:-3]
- elif theText.endswith(" <") and action != nwDocAction.INDENT_L:
+ elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
newText = newText[:-2]
- elif theText.endswith("<<"):
+ elif setText.endswith("<<"):
newText = newText[:-2]
- elif theText.endswith("<") and action != nwDocAction.INDENT_L:
+ elif setText.endswith("<") and action != nwDocAction.INDENT_L:
newText = newText[:-1]
# Apply new format
if action == nwDocAction.BLOCK_COM:
- theText = "% "+newText
+ setText = "% "+newText
cOffset -= 2
elif action == nwDocAction.BLOCK_H1:
- theText = "# "+newText
+ setText = "# "+newText
cOffset -= 2
elif action == nwDocAction.BLOCK_H2:
- theText = "## "+newText
+ setText = "## "+newText
cOffset -= 3
elif action == nwDocAction.BLOCK_H3:
- theText = "### "+newText
+ setText = "### "+newText
cOffset -= 4
elif action == nwDocAction.BLOCK_H4:
- theText = "#### "+newText
+ setText = "#### "+newText
cOffset -= 5
elif action == nwDocAction.BLOCK_TTL:
- theText = "#! "+newText
+ setText = "#! "+newText
cOffset -= 3
elif action == nwDocAction.BLOCK_UNN:
- theText = "##! "+newText
+ setText = "##! "+newText
cOffset -= 4
elif action == nwDocAction.ALIGN_L:
- theText = newText+" <<"
+ setText = newText+" <<"
elif action == nwDocAction.ALIGN_C:
- theText = ">> "+newText+" <<"
+ setText = ">> "+newText+" <<"
cOffset -= 3
elif action == nwDocAction.ALIGN_R:
- theText = ">> "+newText
+ setText = ">> "+newText
cOffset -= 3
elif action == nwDocAction.INDENT_L:
- theText = "> "+newText
+ setText = "> "+newText
cOffset -= 2
elif action == nwDocAction.INDENT_R:
- theText = newText+" <"
+ setText = newText+" <"
elif action == nwDocAction.BLOCK_TXT:
- theText = newText
+ setText = newText
else:
logger.error("Unknown or unsupported block format requested: '%s'", str(action))
return False
# Replace the block text
- theCursor.beginEditBlock()
- posO = theCursor.position()
- theCursor.select(QTextCursor.BlockUnderCursor)
- posS = theCursor.selectionStart()
- theCursor.removeSelectedText()
- theCursor.setPosition(posS)
+ cursor.beginEditBlock()
+ posO = cursor.position()
+ cursor.select(QTextCursor.BlockUnderCursor)
+ posS = cursor.selectionStart()
+ cursor.removeSelectedText()
+ cursor.setPosition(posS)
if posS > 0 and hasText:
# If the block already had text, we must insert a new block
# first before we can add back the text to it.
- theCursor.insertBlock()
+ cursor.insertBlock()
- theCursor.insertText(theText)
+ cursor.insertText(setText)
if posO - cOffset >= 0:
- theCursor.setPosition(posO - cOffset)
+ cursor.setPosition(posO - cOffset)
- theCursor.endEditBlock()
- self.setTextCursor(theCursor)
+ cursor.endEditBlock()
+ self.setTextCursor(cursor)
return True
@@ -1706,37 +1705,37 @@ class GuiDocEditor(QPlainTextEdit):
if cursor is None:
cursor = self.textCursor()
- theBlock = cursor.block()
- theText = theBlock.text()
+ block = cursor.block()
+ text = block.text()
- if len(theText) == 0:
+ if len(text) == 0:
return False
- if theText.startswith("@"):
+ if text.startswith("@"):
- isGood, tBits, tPos = SHARED.project.index.scanThis(theText)
+ isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if not isGood:
return False
- theTag = ""
- cPos = cursor.selectionStart() - theBlock.position()
+ tag = ""
+ cPos = cursor.selectionStart() - block.position()
for sTag, sPos in zip(reversed(tBits), reversed(tPos)):
if cPos >= sPos:
# The cursor is between the start of two tags
if cPos <= sPos + len(sTag):
# The cursor is inside or at the edge of the tag
- theTag = sTag
+ tag = sTag
break
- if not theTag or theTag.startswith("@"):
+ if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that
return False
if loadTag:
- logger.debug("Attempting to follow tag '%s'", theTag)
- self.loadDocumentTagRequest.emit(theTag, nwDocMode.VIEW)
+ logger.debug("Attempting to follow tag '%s'", tag)
+ self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
else:
- logger.debug("Potential tag '%s'", theTag)
+ logger.debug("Potential tag '%s'", tag)
return True
@@ -1752,94 +1751,93 @@ class GuiDocEditor(QPlainTextEdit):
if not block.isValid():
return
- theText = block.text()
- theCursor = self.textCursor()
- thePos = theCursor.positionInBlock()
- theLen = len(theText)
+ text = block.text()
+ cursor = self.textCursor()
+ tPos = cursor.positionInBlock()
+ tLen = len(text)
- if theLen < 1 or thePos-1 > theLen:
+ if tLen < 1 or tPos-1 > tLen:
return
- theOne = theText[thePos-1:thePos]
- theTwo = theText[thePos-2:thePos]
- theThree = theText[thePos-3:thePos]
+ tOne = text[tPos-1:tPos]
+ tTwo = text[tPos-2:tPos]
+ tThree = text[tPos-3:tPos]
- if not theOne:
- # Sorry, Neo and Zathras
+ if not tOne:
return
nDelete = 0
- tInsert = theOne
+ tInsert = tOne
- if self._typRepDQuote and theTwo[:1].isspace() and theTwo.endswith('"'):
+ if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
nDelete = 1
tInsert = self._typDQuoteO
- elif self._typRepDQuote and theOne == '"':
+ elif self._typRepDQuote and tOne == '"':
nDelete = 1
- if thePos == 1:
+ if tPos == 1:
tInsert = self._typDQuoteO
- elif thePos == 2 and theTwo == '>"':
+ elif tPos == 2 and tTwo == '>"':
tInsert = self._typDQuoteO
- elif thePos == 3 and theThree == '>>"':
+ elif tPos == 3 and tThree == '>>"':
tInsert = self._typDQuoteO
else:
tInsert = self._typDQuoteC
- elif self._typRepSQuote and theTwo[:1].isspace() and theTwo.endswith("'"):
+ elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
nDelete = 1
tInsert = self._typSQuoteO
- elif self._typRepSQuote and theOne == "'":
+ elif self._typRepSQuote and tOne == "'":
nDelete = 1
- if thePos == 1:
+ if tPos == 1:
tInsert = self._typSQuoteO
- elif thePos == 2 and theTwo == ">'":
+ elif tPos == 2 and tTwo == ">'":
tInsert = self._typSQuoteO
- elif thePos == 3 and theThree == ">>'":
+ elif tPos == 3 and tThree == ">>'":
tInsert = self._typSQuoteO
else:
tInsert = self._typSQuoteC
- elif self._typRepDash and theThree == "---":
+ elif self._typRepDash and tThree == "---":
nDelete = 3
tInsert = nwUnicode.U_EMDASH
- elif self._typRepDash and theTwo == "--":
+ elif self._typRepDash and tTwo == "--":
nDelete = 2
tInsert = nwUnicode.U_ENDASH
- elif self._typRepDash and theTwo == nwUnicode.U_ENDASH + "-":
+ elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
nDelete = 2
tInsert = nwUnicode.U_EMDASH
- elif self._typRepDots and theThree == "...":
+ elif self._typRepDots and tThree == "...":
nDelete = 3
tInsert = nwUnicode.U_HELLIP
- elif theOne == nwUnicode.U_LSEP:
+ elif tOne == nwUnicode.U_LSEP:
# This resolves issue #1150
nDelete = 1
tInsert = nwUnicode.U_PSEP
tCheck = tInsert
if self._typPadBefore and tCheck in self._typPadBefore:
- if self._allowSpaceBeforeColon(theText, tCheck):
+ if self._allowSpaceBeforeColon(text, tCheck):
nDelete = max(nDelete, 1)
- chkPos = thePos - nDelete - 1
- if chkPos >= 0 and theText[chkPos].isspace():
+ chkPos = tPos - nDelete - 1
+ if chkPos >= 0 and text[chkPos].isspace():
# Strip existing space before inserting a new (#1061)
nDelete += 1
tInsert = self._typPadChar + tInsert
if self._typPadAfter and tCheck in self._typPadAfter:
- if self._allowSpaceBeforeColon(theText, tCheck):
+ if self._allowSpaceBeforeColon(text, tCheck):
nDelete = max(nDelete, 1)
tInsert = tInsert + self._typPadChar
if nDelete > 0:
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
- theCursor.insertText(tInsert)
+ cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
+ cursor.insertText(tInsert)
return
@@ -1861,11 +1859,11 @@ class GuiDocEditor(QPlainTextEdit):
"""Return a cursor which may or may not have a selection based
on user settings and document action.
"""
- theCursor = self.textCursor()
- if CONFIG.autoSelect and not theCursor.hasSelection():
- theCursor.select(QTextCursor.WordUnderCursor)
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
+ cursor = self.textCursor()
+ if CONFIG.autoSelect and not cursor.hasSelection():
+ cursor.select(QTextCursor.WordUnderCursor)
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
# Underscore counts as a part of the word, so check that the
# selection isn't wrapped in italics markers.
@@ -1877,43 +1875,41 @@ class GuiDocEditor(QPlainTextEdit):
posE -= 1
reSelect = True
if reSelect:
- theCursor.clearSelection()
- theCursor.setPosition(posS, QTextCursor.MoveAnchor)
- theCursor.setPosition(posE-1, QTextCursor.KeepAnchor)
+ cursor.clearSelection()
+ cursor.setPosition(posS, QTextCursor.MoveAnchor)
+ cursor.setPosition(posE-1, QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ self.setTextCursor(cursor)
- return theCursor
+ return cursor
def _makeSelection(self, mode: QTextCursor.SelectionType) -> None:
- """Select text based on a selection mode."""
- theCursor = self.textCursor()
- theCursor.clearSelection()
- theCursor.select(mode)
+ """Select text based on selection mode."""
+ cursor = self.textCursor()
+ cursor.clearSelection()
+ cursor.select(mode)
if mode == QTextCursor.WordUnderCursor:
- theCursor = self._autoSelect()
+ cursor = self._autoSelect()
elif mode == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding paragraph
# separator, which we want to avoid.
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
- selTxt = theCursor.selectedText()
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
+ selTxt = cursor.selectedText()
if selTxt.startswith(nwUnicode.U_PSEP):
- theCursor.setPosition(posS+1, QTextCursor.MoveAnchor)
- theCursor.setPosition(posE, QTextCursor.KeepAnchor)
+ cursor.setPosition(posS+1, QTextCursor.MoveAnchor)
+ cursor.setPosition(posE, QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ self.setTextCursor(cursor)
return
def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None:
- """Wrapper function to select text based on selection mode, but
- first move cursor to given position.
- """
- theCursor = self.cursorForPosition(pos)
- self.setTextCursor(theCursor)
+ """Select text based on selection mode, but first move cursor."""
+ cursor = self.cursorForPosition(pos)
+ self.setTextCursor(cursor)
self._makeSelection(mode)
return
@@ -1956,11 +1952,11 @@ class BackgroundWordCounter(QRunnable):
"""
self._isRunning = True
if self._forSelection:
- theText = self._docEditor.textCursor().selectedText()
+ text = self._docEditor.textCursor().selectedText()
else:
- theText = self._docEditor.getText()
+ text = self._docEditor.getText()
- cC, wC, pC = countWords(theText)
+ cC, wC, pC = countWords(text)
self.signals.countsReady.emit(cC, wC, pC)
self._isRunning = False
@@ -2411,18 +2407,18 @@ class GuiDocEditHeader(QWidget):
self.setAutoFillBackground(True)
# Title Label
- self.theTitle = QLabel()
- self.theTitle.setText("")
- self.theTitle.setIndent(0)
- self.theTitle.setMargin(0)
- self.theTitle.setContentsMargins(0, 0, 0, 0)
- self.theTitle.setAutoFillBackground(True)
- self.theTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
- self.theTitle.setFixedHeight(fPx)
+ self.itemTitle = QLabel()
+ self.itemTitle.setText("")
+ self.itemTitle.setIndent(0)
+ self.itemTitle.setMargin(0)
+ self.itemTitle.setContentsMargins(0, 0, 0, 0)
+ self.itemTitle.setAutoFillBackground(True)
+ self.itemTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
+ self.itemTitle.setFixedHeight(fPx)
- lblFont = self.theTitle.font()
+ lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
- self.theTitle.setFont(lblFont)
+ self.itemTitle.setFont(lblFont)
# Buttons
self.editButton = QToolButton(self)
@@ -2466,7 +2462,7 @@ class GuiDocEditHeader(QWidget):
self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.editButton, 0)
self.outerBox.addWidget(self.searchButton, 0)
- self.outerBox.addWidget(self.theTitle, 1)
+ self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addWidget(self.minmaxButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
self.setLayout(self.outerBox)
@@ -2513,13 +2509,13 @@ class GuiDocEditHeader(QWidget):
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
"""
- thePalette = QPalette()
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
+ palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
- self.setPalette(thePalette)
- self.theTitle.setPalette(thePalette)
+ self.setPalette(palette)
+ self.itemTitle.setPalette(palette)
return
@@ -2529,7 +2525,7 @@ class GuiDocEditHeader(QWidget):
"""
self._docHandle = tHandle
if tHandle is None:
- self.theTitle.setText("")
+ self.itemTitle.setText("")
self.editButton.setVisible(False)
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
@@ -2545,12 +2541,12 @@ class GuiDocEditHeader(QWidget):
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
- self.theTitle.setText(sSep.join(tTitle))
+ self.itemTitle.setText(sSep.join(tTitle))
else:
nwItem = pTree[tHandle]
if nwItem is None:
return False
- self.theTitle.setText(nwItem.itemName)
+ self.itemTitle.setText(nwItem.itemName)
self.editButton.setVisible(True)
self.searchButton.setVisible(True)
@@ -2631,7 +2627,7 @@ class GuiDocEditFooter(QWidget):
self.docEditor = docEditor
self.mainGui = docEditor.mainGui
- self._theItem = None
+ self._tItem = None
self._docHandle = None
self._docSelection = False
@@ -2737,15 +2733,15 @@ class GuiDocEditFooter(QWidget):
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
"""
- thePalette = QPalette()
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
+ palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
- self.setPalette(thePalette)
- self.statusText.setPalette(thePalette)
- self.linesText.setPalette(thePalette)
- self.wordsText.setPalette(thePalette)
+ self.setPalette(palette)
+ self.statusText.setPalette(palette)
+ self.linesText.setPalette(palette)
+ self.wordsText.setPalette(palette)
return
@@ -2754,9 +2750,9 @@ class GuiDocEditFooter(QWidget):
self._docHandle = tHandle
if self._docHandle is None:
logger.debug("No handle set, so clearing the editor footer")
- self._theItem = None
+ self._tItem = None
else:
- self._theItem = SHARED.project.tree[self._docHandle]
+ self._tItem = SHARED.project.tree[self._docHandle]
self.setHasSelection(False)
self.updateInfo()
@@ -2773,13 +2769,13 @@ class GuiDocEditFooter(QWidget):
def updateInfo(self) -> None:
"""Update the content of text labels."""
- if self._theItem is None:
+ if self._tItem is None:
sIcon = QPixmap()
sText = ""
else:
- theStatus, theIcon = self._theItem.getImportStatus(incIcon=True)
- sIcon = theIcon.pixmap(self.sPx, self.sPx)
- sText = f"{theStatus} / {self._theItem.describeMe()}"
+ status, icon = self._tItem.getImportStatus(incIcon=True)
+ sIcon = icon.pixmap(self.sPx, self.sPx)
+ sText = f"{status} / {self._tItem.describeMe()}"
self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText)
@@ -2788,12 +2784,12 @@ class GuiDocEditFooter(QWidget):
def updateLineCount(self) -> None:
"""Update the line counter."""
- if self._theItem is None:
+ if self._tItem is None:
iLine = 0
iDist = 0
else:
- theCursor = self.docEditor.textCursor()
- iLine = theCursor.blockNumber() + 1
+ cursor = self.docEditor.textCursor()
+ iLine = cursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor._qDocument.blockCount()
self.linesText.setText(
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
@@ -2814,12 +2810,12 @@ class GuiDocEditFooter(QWidget):
def _updateWordCounts(self) -> None:
"""Update the word count for the whole document."""
- if self._theItem is None:
+ if self._tItem is None:
wCount = 0
wDiff = 0
else:
- wCount = self._theItem.wordCount
- wDiff = wCount - self._theItem.initCount
+ wCount = self._tItem.wordCount
+ wDiff = wCount - self._tItem.initCount
self.wordsText.setText(
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
From 309c7de5de3d554c85d84a9dcc48c98ff0422917 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 14 Sep 2023 16:20:38 +0200
Subject: [PATCH 07/15] Clean up variables in editor
---
novelwriter/gui/doceditor.py | 36 +++++++++---------------------------
1 file changed, 9 insertions(+), 27 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index eedbca6f..6f7ced9f 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -93,18 +93,15 @@ class GuiDocEditor(QPlainTextEdit):
self._nwItem = None
self._docChanged = False # Flag for changed status of document
- self._docHandle = None # The handle of the open file
+ self._docHandle = None # The handle of the open document
self._spellCheck = False # Flag for spell checking enabled
self._nonWord = "\"'" # Characters to not include in spell checking
self._vpMargin = 0 # The editor viewport margin, set during init
# Document Variables
- self._charCount = 0 # Character count
- self._wordCount = 0 # Word count
- self._paraCount = 0 # Paragraph count
- self._lastEdit = 0 # Time stamp of last edit
- self._lastActive = 0.0 # Time stamp of last activity
+ self._lastEdit = 0.0 # Timestamp of last edit
+ self._lastActive = 0.0 # Timestamp of last activity
self._lastFind = None # Position of the last found search word
self._doReplace = False # Switch to temporarily disable auto-replace
@@ -230,10 +227,7 @@ class GuiDocEditor(QPlainTextEdit):
self.wcTimerSel.stop()
self._docHandle = None
- self._charCount = 0
- self._wordCount = 0
- self._paraCount = 0
- self._lastEdit = 0
+ self._lastEdit = 0.0
self._lastActive = 0.0
self._lastFind = None
self._doReplace = False
@@ -456,14 +450,9 @@ class GuiDocEditor(QPlainTextEdit):
return False
docText = self.getText()
-
cC, wC, pC = countWords(docText)
self._updateDocCounts(cC, wC, pC)
- self._nwItem.setCharCount(self._charCount)
- self._nwItem.setWordCount(self._wordCount)
- self._nwItem.setParaCount(self._paraCount)
-
self.saveCursorPosition()
if not self._nwDocument.writeDocument(docText):
saveOk = False
@@ -911,14 +900,15 @@ class GuiDocEditor(QPlainTextEdit):
if CONFIG.autoScroll:
cPos = self.cursorRect().topLeft().y()
super().keyPressEvent(event)
+ nPos = self.cursorRect().topLeft().y()
kMod = event.modifiers()
okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier
okKey = event.key() not in self.MOVE_KEYS
- if okMod and okKey:
+ if nPos != cPos and okMod and okKey:
mPos = CONFIG.autoScrollPos*0.01 * self.viewport().height()
if cPos > mPos:
vBar = self.verticalScrollBar()
- vBar.setValue(vBar.value() + 1)
+ vBar.setValue(vBar.value() + (1 if nPos > cPos else -1))
else:
super().keyPressEvent(event)
@@ -1095,7 +1085,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Word counter is busy")
return
- if time() - self._lastEdit < 5 * self.wcInterval:
+ if time() - self._lastEdit < 5.0 * self.wcInterval:
logger.debug("Running word counter")
SHARED.runInThreadPool(self.wCounterDoc)
@@ -1109,10 +1099,6 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Updating word count")
- self._charCount = cCount
- self._wordCount = wCount
- self._paraCount = pCount
-
self._nwItem.setCharCount(cCount)
self._nwItem.setWordCount(wCount)
self._nwItem.setParaCount(pCount)
@@ -1132,12 +1118,10 @@ class GuiDocEditor(QPlainTextEdit):
if not self.wcTimerSel.isActive():
self.wcTimerSel.start()
self.docFooter.setHasSelection(True)
-
else:
self.wcTimerSel.stop()
self.docFooter.setHasSelection(False)
self.docFooter.updateCounts()
-
return
@pyqtSlot()
@@ -1270,7 +1254,7 @@ class GuiDocEditor(QPlainTextEdit):
self.setTextCursor(cursor)
# Search up to a maximum of 1000, and make sure certain special
- # searches like a regex search for .* turns into an infinite loop
+ # searches like a regex search for .* don't loop infinitely
while self.find(searchFor, findOpt) and len(resE) <= 1000:
cursor = self.textCursor()
if cursor.hasSelection():
@@ -1987,7 +1971,6 @@ class GuiDocEditSearch(QFrame):
logger.debug("Create: GuiDocEditSearch")
self.docEditor = docEditor
- self.mainGui = docEditor.mainGui
self.repVisible = False
self.isCaseSense = CONFIG.searchCase
@@ -2625,7 +2608,6 @@ class GuiDocEditFooter(QWidget):
logger.debug("Create: GuiDocEditFooter")
self.docEditor = docEditor
- self.mainGui = docEditor.mainGui
self._tItem = None
self._docHandle = None
From c3a4b840fb69b0347b286691c400d7eab5efa3f0 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 14 Sep 2023 16:26:05 +0200
Subject: [PATCH 08/15] Improve handling of spell check state
---
novelwriter/gui/doceditor.py | 29 +++++++++++++----------------
novelwriter/gui/dochighlight.py | 16 +++++++---------
novelwriter/gui/editordocument.py | 12 +++++++++++-
novelwriter/gui/mainmenu.py | 5 +++--
novelwriter/guimain.py | 1 +
5 files changed, 35 insertions(+), 28 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 6f7ced9f..60c7b00e 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -80,6 +80,7 @@ class GuiDocEditor(QPlainTextEdit):
loadDocumentTagRequest = pyqtSignal(str, Enum)
novelStructureChanged = pyqtSignal()
novelItemMetaChanged = pyqtSignal(str)
+ spellCheckStateChanged = pyqtSignal(bool)
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
@@ -95,7 +96,6 @@ class GuiDocEditor(QPlainTextEdit):
self._docChanged = False # Flag for changed status of document
self._docHandle = None # The handle of the open document
- self._spellCheck = False # Flag for spell checking enabled
self._nonWord = "\"'" # Characters to not include in spell checking
self._vpMargin = 0 # The editor viewport margin, set during init
@@ -125,6 +125,7 @@ class GuiDocEditor(QPlainTextEdit):
# Connect Signals
self._qDocument.contentsChange.connect(self._docChange)
self.selectionChanged.connect(self._updateSelectedStatus)
+ self.spellCheckStateChanged.connect(self._qDocument.setSpellCheckState)
# Document Title
self.docHeader = GuiDocEditHeader(self)
@@ -609,25 +610,21 @@ class GuiDocEditor(QPlainTextEdit):
current status saved in this class.
"""
if state is None:
- state = not self._spellCheck
-
- if not CONFIG.hasEnchant:
- if state:
- SHARED.info(self.tr(
- "Spell checking requires the package PyEnchant. "
- "It does not appear to be installed."
- ))
- state = False
+ state = not SHARED.project.data.spellCheck
if SHARED.spelling.spellLanguage is None:
state = False
- self._spellCheck = state
- self.mainGui.mainMenu.setSpellCheck(state)
+ if state and not CONFIG.hasEnchant:
+ SHARED.info(self.tr(
+ "Spell checking requires the package PyEnchant. "
+ "It does not appear to be installed."
+ ))
+ state = False
+
SHARED.project.data.setSpellCheck(state)
- self._qDocument.syntaxHighlighter.setSpellCheck(state)
- if state is False:
- self.spellCheckDocument()
+ self.spellCheckStateChanged.emit(state)
+ self.spellCheckDocument()
logger.debug("Spell check is set to '%s'", str(state))
@@ -1021,7 +1018,7 @@ class GuiDocEditor(QPlainTextEdit):
aSPar.triggered.connect(lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, pos))
# Spell Checking
- if self._spellCheck:
+ if SHARED.project.data.spellCheck:
word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position())
if word and cPos >= 0 and cLen > 0:
logger.debug("Word '%s' is misspelled", word)
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 9bc8d953..64d1f34e 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -376,20 +376,18 @@ class GuiDocHighlighter(QSyntaxHighlighter):
spFmt.merge(xFmt[xM])
self.setFormat(x, 1, spFmt)
- if not self._spellCheck:
- return
-
data = self.currentBlockUserData()
if not isinstance(data, TextBlockData):
data = TextBlockData()
self.setCurrentBlockUserData(data)
- for xPos, xLen in data.spellCheck(text):
- for x in range(xPos, xPos+xLen):
- spFmt = self.format(x)
- spFmt.setUnderlineColor(self._colSpell)
- spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
- self.setFormat(x, 1, spFmt)
+ if self._spellCheck:
+ for xPos, xLen in data.spellCheck(text):
+ for x in range(xPos, xPos+xLen):
+ spFmt = self.format(x)
+ spFmt.setUnderlineColor(self._colSpell)
+ spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ self.setFormat(x, 1, spFmt)
return
diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py
index 793d82bc..160e85ed 100644
--- a/novelwriter/gui/editordocument.py
+++ b/novelwriter/gui/editordocument.py
@@ -28,7 +28,7 @@ import logging
from time import time
from PyQt5.QtGui import QTextCursor, QTextDocument
-from PyQt5.QtCore import QObject
+from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp
from novelwriter import SHARED
@@ -113,4 +113,14 @@ class GuiTextDocument(QTextDocument):
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, []
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(bool)
+ def setSpellCheckState(self, state: bool) -> None:
+ """Set the spell check state of the syntax highlighter."""
+ self._syntax.setSpellCheck(state)
+ return
+
# END Class GuiTextDocument
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 55813a71..45374ada 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -78,10 +78,11 @@ class GuiMainMenu(QMenuBar):
return
##
- # Update Menu on Settings Changed
+ # Public Slots
##
- def setSpellCheck(self, state: bool) -> None:
+ @pyqtSlot(bool)
+ def setSpellCheckState(self, state: bool) -> None:
"""Forward spell check check state to its action."""
self.aSpellCheck.setChecked(state)
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index b19f585f..4e792a04 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -266,6 +266,7 @@ class GuiMain(QMainWindow):
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
+ self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
From cb522c7d5090cd3bcf17932fcdc17f6cc981c2c6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 15 Sep 2023 18:40:32 +0200
Subject: [PATCH 09/15] Fix some layout issues in main gui
---
novelwriter/guimain.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 4e792a04..a5c55fd7 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -152,7 +152,7 @@ class GuiMain(QMainWindow):
# Project Tree View
self.treePane = QWidget(self)
- self.treeBox = QVBoxLayout(self)
+ self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0)
self.treeBox.setSpacing(mPx)
self.treeBox.addWidget(self.projStack)
@@ -222,7 +222,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
# Assemble Main Window Elements
- self.mainBox = QHBoxLayout(self)
+ self.mainBox = QHBoxLayout()
self.mainBox.addWidget(self.sideBar)
self.mainBox.addWidget(self.mainStack)
self.mainBox.setContentsMargins(0, 0, 0, 0)
From 0700dd59bb9f865370d4f25a905f73a899429fe1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 26 Sep 2023 18:39:50 +0200
Subject: [PATCH 10/15] Clean up variables and typing in doc converters
---
novelwriter/core/tohtml.py | 64 ++++++++++++++++++--------------------
novelwriter/core/tomd.py | 53 +++++++++++++++----------------
novelwriter/core/toodt.py | 40 ++++++++++++------------
3 files changed, 75 insertions(+), 82 deletions(-)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 8459ae59..a4d4bb3f 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -111,7 +111,7 @@ class ToHtml(Tokenizer):
def getFullResultSize(self) -> int:
"""Return the size of the full HTML result."""
- return sum([len(x) for x in self._fullHTML])
+ return sum(len(x) for x in self._fullHTML)
def doPreProcessing(self) -> None:
"""Extend the auto-replace to also properly encode some unicode
@@ -122,9 +122,7 @@ class ToHtml(Tokenizer):
return
def doConvert(self) -> None:
- """Convert the list of text tokens into a HTML document saved
- to _result.
- """
+ """Convert the list of text tokens into an HTML document."""
if self._genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "",
@@ -160,9 +158,9 @@ class ToHtml(Tokenizer):
self._result = ""
- thisPar = []
- parStyle = None
- tmpResult = []
+ para = []
+ pStyle = None
+ lines = []
for tType, tLine, tText, tFormat, tStyle in self._tokens:
@@ -231,69 +229,67 @@ class ToHtml(Tokenizer):
# Process Text Type
if tType == self.T_EMPTY:
- if parStyle is None:
- parStyle = ""
- if len(thisPar) > 1 and self._cssStyles:
- parClass = " class='break'"
+ if pStyle is None:
+ pStyle = ""
+ if len(para) > 1 and self._cssStyles:
+ pClass = " class='break'"
else:
- parClass = ""
- if len(thisPar) > 0:
- tTemp = "
".join(thisPar)
- tmpResult.append(f"{tTemp.rstrip()}
\n")
- thisPar = []
- parStyle = None
+ pClass = ""
+ if len(para) > 0:
+ tTemp = "
".join(para)
+ lines.append(f"{tTemp.rstrip()}
\n")
+ para = []
+ pStyle = None
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"{aNm}{tHead}
\n")
+ lines.append(f"{aNm}{tHead}
\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
+ lines.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
+ lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
+ lines.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
+ lines.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "
")
- tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
+ lines.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
elif tType == self.T_SEP:
- tmpResult.append(f"{tText}
\n")
+ lines.append(f"{tText}
\n")
elif tType == self.T_SKIP:
- tmpResult.append(f"
\n")
+ lines.append(f"
\n")
elif tType == self.T_TEXT:
tTemp = tText
- if parStyle is None:
- parStyle = hStyle
+ if pStyle is None:
+ pStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
- thisPar.append(stripEscape(tTemp.rstrip()))
+ para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis:
- tmpResult.append(self._formatSynopsis(tText))
+ lines.append(self._formatSynopsis(tText))
elif tType == self.T_COMMENT and self._doComments:
- tmpResult.append(self._formatComments(tText))
+ lines.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self._doKeywords:
tTemp = f"{self._formatKeywords(tText)}
\n"
- tmpResult.append(tTemp)
-
- self._result = "".join(tmpResult)
- tmpResult = []
+ lines.append(tTemp)
+ self._result = "".join(lines)
if self._genMode != self.M_PREVIEW:
self._fullHTML.append(self._result)
diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py
index c9859318..ec1646ad 100644
--- a/novelwriter/core/tomd.py
+++ b/novelwriter/core/tomd.py
@@ -65,10 +65,12 @@ class ToMarkdown(Tokenizer):
##
def setStandardMarkdown(self) -> None:
+ """Set the converter to use standard Markdown formatting."""
self._genMode = self.M_STD
return
def setGitHubMarkdown(self) -> None:
+ """Set the converter to use GitHub Markdown formatting."""
self._genMode = self.M_GH
return
@@ -78,12 +80,10 @@ class ToMarkdown(Tokenizer):
def getFullResultSize(self) -> int:
"""Return the size of the full Markdown result."""
- return sum([len(x) for x in self._fullMD])
+ return sum(len(x) for x in self._fullMD)
def doConvert(self) -> None:
- """Convert the list of text tokens into a HTML document saved
- to theResult.
- """
+ """Convert the list of text tokens into a Markdown document."""
if self._genMode == self.M_STD:
# Standard
mdTags = {
@@ -107,68 +107,65 @@ class ToMarkdown(Tokenizer):
self._result = ""
- thisPar = []
- tmpResult = []
+ para = []
+ lines = []
for tType, _, tText, tFormat, tStyle in self._tokens:
- # Process Text Type
if tType == self.T_EMPTY:
- if len(thisPar) > 0:
- tTemp = (" \n".join(thisPar)).rstrip(" ")
- tmpResult.append(f"{tTemp}\n\n")
- thisPar = []
+ if len(para) > 0:
+ tTemp = (" \n".join(para)).rstrip(" ")
+ lines.append(f"{tTemp}\n\n")
+ para = []
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"# {tHead}\n\n")
+ lines.append(f"# {tHead}\n\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"## {tHead}\n\n")
+ lines.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"# {tHead}\n\n")
+ lines.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"## {tHead}\n\n")
+ lines.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"### {tHead}\n\n")
+ lines.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n")
- tmpResult.append(f"#### {tHead}\n\n")
+ lines.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP:
- tmpResult.append("%s\n\n" % tText)
+ lines.append(f"{tText}\n\n")
elif tType == self.T_SKIP:
- tmpResult.append("\n\n\n")
+ lines.append("\n\n\n")
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
- thisPar.append(tTemp.rstrip())
+ para.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self._doSynopsis:
- locName = self._localLookup("Synopsis")
- tmpResult.append(f"**{locName}:** {tText}\n\n")
+ label = self._localLookup("Synopsis")
+ lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_COMMENT and self._doComments:
- locName = self._localLookup("Comment")
- tmpResult.append(f"**{locName}:** {tText}\n\n")
+ label = self._localLookup("Comment")
+ lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_KEYWORD and self._doKeywords:
- tmpResult.append(self._formatKeywords(tText, tStyle))
-
- self._result = "".join(tmpResult)
- tmpResult = []
+ lines.append(self._formatKeywords(tText, tStyle))
+ self._result = "".join(lines)
self._fullMD.append(self._result)
return
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index b69af17b..4f557dff 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -394,9 +394,9 @@ class ToOdt(Tokenizer):
self.FMT_D_E: "s_", # Strikethrough close format
}
- thisPar = []
- thisFmt = []
- parStyle = None
+ fmt = []
+ para = []
+ pStyle = None
for tType, _, tText, tFormat, tStyle in self._tokens:
# Styles
@@ -429,20 +429,20 @@ class ToOdt(Tokenizer):
# Process Text Types
if tType == self.T_EMPTY:
- if len(thisPar) > 1 and parStyle is not None:
+ if len(para) > 1 and pStyle is not None:
if self._doJustify:
- parStyle.setTextAlign("left")
+ pStyle.setTextAlign("left")
- if len(thisPar) > 0 and parStyle is not None:
- tTemp = "\n".join(thisPar)
- fTemp = " ".join(thisFmt)
+ if len(para) > 0 and pStyle is not None:
+ tTemp = "\n".join(para)
+ fTemp = " ".join(fmt)
tTxt = tTemp.rstrip()
tFmt = fTemp[:len(tTxt)]
- self._addTextPar("Text_20_body", parStyle, tTxt, tFmt=tFmt)
+ self._addTextPar("Text_20_body", pStyle, tTxt, tFmt=tFmt)
- thisPar = []
- thisFmt = []
- parStyle = None
+ fmt = []
+ para = []
+ pStyle = None
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n")
@@ -475,8 +475,8 @@ class ToOdt(Tokenizer):
self._addTextPar("Separator", oStyle, "")
elif tType == self.T_TEXT:
- if parStyle is None:
- parStyle = oStyle
+ if pStyle is None:
+ pStyle = oStyle
tFmt = " "*len(tText)
for xPos, xLen, xFmt in tFormat:
@@ -484,8 +484,8 @@ class ToOdt(Tokenizer):
tTxt = tText.rstrip()
tFmt = tFmt[:len(tTxt)]
- thisPar.append(tTxt)
- thisFmt.append(tFmt)
+ para.append(tTxt)
+ fmt.append(tFmt)
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
@@ -501,8 +501,8 @@ class ToOdt(Tokenizer):
return
- def closeDocument(self):
- """Return the serialised XML document"""
+ def closeDocument(self) -> None:
+ """Pack the styles of the XML document."""
# Build the auto-generated styles
for styleName, styleObj in self._autoPara.values():
styleObj.packXML(self._xAuto, styleName)
@@ -510,7 +510,7 @@ class ToOdt(Tokenizer):
styleObj.packXML(self._xAuto, styleName)
return
- def saveFlatXML(self, path: str | Path):
+ def saveFlatXML(self, path: str | Path) -> None:
"""Save the data to an .fodt file."""
with open(path, mode="wb") as fObj:
xml = ET.ElementTree(self._dFlat)
@@ -519,7 +519,7 @@ class ToOdt(Tokenizer):
logger.info("Wrote file: %s", path)
return
- def saveOpenDocText(self, path: str | Path):
+ def saveOpenDocText(self, path: str | Path) -> None:
"""Save the data to an .odt file."""
mMani = _mkTag("manifest", "manifest")
mVers = _mkTag("manifest", "version")
From fd76f5a2c49d8f4a17d130a4c281e72fb180a00a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 28 Sep 2023 14:33:33 +0200
Subject: [PATCH 11/15] Do some minor variable cleanup in ToOdt
---
novelwriter/core/toodt.py | 78 +++++++++++++++++++--------------------
1 file changed, 39 insertions(+), 39 deletions(-)
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 4f557dff..0652c459 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -718,9 +718,9 @@ class ToOdt(Tokenizer):
return newName
- def _emToCm(self, emVal: float) -> str:
+ def _emToCm(self, value: float) -> str:
"""Converts an em value to centimetres."""
- return f"{emVal*2.54/72*self._textSize:.3f}cm"
+ return f"{value*2.54/72*self._textSize:.3f}cm"
##
# Style Elements
@@ -1193,56 +1193,56 @@ class ODTParagraphStyle:
# Methods
##
- def checkNew(self, refStyle: ODTParagraphStyle) -> bool:
+ def checkNew(self, style: ODTParagraphStyle) -> bool:
"""Check if there are new settings in refStyle that differ from
those in the current object.
"""
- for aName, (_, aVal) in refStyle._mAttr.items():
- if aVal is not None and aVal != self._mAttr[aName][1]:
+ for name, (_, aVal) in style._mAttr.items():
+ if aVal is not None and aVal != self._mAttr[name][1]:
return True
- for aName, (_, aVal) in refStyle._pAttr.items():
- if aVal is not None and aVal != self._pAttr[aName][1]:
+ for name, (_, aVal) in style._pAttr.items():
+ if aVal is not None and aVal != self._pAttr[name][1]:
return True
- for aName, (_, aVal) in refStyle._tAttr.items():
- if aVal is not None and aVal != self._tAttr[aName][1]:
+ for name, (_, aVal) in style._tAttr.items():
+ if aVal is not None and aVal != self._tAttr[name][1]:
return True
return False
def getID(self) -> str:
"""Generate a unique ID from the settings."""
- theString = (
+ string = (
f"Paragraph:Main:{str(self._mAttr)}:"
f"Paragraph:Para:{str(self._pAttr)}:"
f"Paragraph:Text:{str(self._tAttr)}:"
)
- return sha256(theString.encode()).hexdigest()
+ return sha256(string.encode()).hexdigest()
def packXML(self, xParent: ET.Element, name: str) -> None:
"""Pack the content into an xml element."""
- theAttr = {}
- theAttr[_mkTag("style", "name")] = name
- theAttr[_mkTag("style", "family")] = "paragraph"
+ attr = {}
+ attr[_mkTag("style", "name")] = name
+ attr[_mkTag("style", "family")] = "paragraph"
for aName, (aNm, aVal) in self._mAttr.items():
if aVal is not None:
- theAttr[_mkTag(aNm, aName)] = aVal
+ attr[_mkTag(aNm, aName)] = aVal
- xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
- theAttr = {}
+ attr = {}
for aName, (aNm, aVal) in self._pAttr.items():
if aVal is not None:
- theAttr[_mkTag(aNm, aName)] = aVal
+ attr[_mkTag(aNm, aName)] = aVal
- if theAttr:
- ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
+ if attr:
+ ET.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=attr)
- theAttr = {}
+ attr = {}
for aName, (aNm, aVal) in self._tAttr.items():
if aVal is not None:
- theAttr[_mkTag(aNm, aName)] = aVal
+ attr[_mkTag(aNm, aName)] = aVal
- if theAttr:
- ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+ if attr:
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
return
@@ -1307,18 +1307,18 @@ class ODTTextStyle:
def packXML(self, xParent: ET.Element, name: str) -> None:
"""Pack the content into an xml element."""
- theAttr = {}
- theAttr[_mkTag("style", "name")] = name
- theAttr[_mkTag("style", "family")] = "text"
- xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
+ attr = {}
+ attr[_mkTag("style", "name")] = name
+ attr[_mkTag("style", "family")] = "text"
+ xEntry = ET.SubElement(xParent, _mkTag("style", "style"), attrib=attr)
- theAttr = {}
+ attr = {}
for aName, (aNm, aVal) in self._tAttr.items():
if aVal is not None:
- theAttr[_mkTag(aNm, aName)] = aVal
+ attr[_mkTag(aNm, aName)] = aVal
- if theAttr:
- ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
+ if attr:
+ ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
return
@@ -1368,17 +1368,17 @@ class XMLParagraph:
return
- def appendText(self, tText: str) -> None:
+ def appendText(self, text: str) -> None:
"""Append text to the XML element. We do this one character at
the time in order to be able to process line breaks, tabs and
spaces separately. Multiple spaces are concatenated into a
single tag, and must therefore be processed separately.
"""
- tText = stripEscape(tText)
+ text = stripEscape(text)
nSpaces = 0
- self._rawTxt += tText
+ self._rawTxt += text
- for c in tText:
+ for c in text:
if c == " ":
nSpaces += 1
continue
@@ -1433,17 +1433,17 @@ class XMLParagraph:
return
- def appendSpan(self, tText: str, tFmt: str) -> None:
+ def appendSpan(self, text: str, fmt: str) -> None:
"""Append a text span to the XML element. The span is always
closed since we do not allow nested spans (like Libre Office).
Therefore we return to the root element level when we're done
processing the text of the span.
"""
- self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: tFmt})
+ self._xTail = ET.SubElement(self._xRoot, TAG_SPAN, attrib={TAG_STNM: fmt})
self._xTail.text = "" # Defaults to None
self._xTail.tail = "" # Defaults to None
self._nState = X_SPAN_TEXT
- self.appendText(tText)
+ self.appendText(text)
self._nState = X_ROOT_TAIL
return
From 699bf2292cf8a315a18f311943e44c16b8eae7ab Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 28 Sep 2023 16:41:37 +0200
Subject: [PATCH 12/15] Clean up import order
---
novelwriter/core/toodt.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 0652c459..c4f6b19d 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -27,10 +27,10 @@ along with this program. If not, see .
from __future__ import annotations
import logging
-from pathlib import Path
import xml.etree.ElementTree as ET
from hashlib import sha256
+from pathlib import Path
from zipfile import ZipFile
from datetime import datetime
From 9dd8f657953d23328439555bbffe5c8bccc94374 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 17 Oct 2023 20:25:17 +0200
Subject: [PATCH 13/15] Clean up a few bits in the editor code, fix margins,
and fix issue in spell checker
---
novelwriter/gui/doceditor.py | 14 ++------------
novelwriter/gui/dochighlight.py | 2 +-
2 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 60c7b00e..525fd179 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -95,8 +95,6 @@ class GuiDocEditor(QPlainTextEdit):
self._docChanged = False # Flag for changed status of document
self._docHandle = None # The handle of the open document
-
- self._nonWord = "\"'" # Characters to not include in spell checking
self._vpMargin = 0 # The editor viewport margin, set during init
# Document Variables
@@ -271,13 +269,6 @@ class GuiDocEditor(QPlainTextEdit):
settings. This function is both called when the editor is
created, and when the user changes the main editor preferences.
"""
- # Some Constants
- self._nonWord = (
- "\"'"
- f"{CONFIG.fmtSQuoteOpen}{CONFIG.fmtSQuoteClose}"
- f"{CONFIG.fmtDQuoteOpen}{CONFIG.fmtDQuoteClose}"
- )
-
# Typography
if CONFIG.fmtPadThin:
self._typPadChar = nwUnicode.U_THNBSP
@@ -307,9 +298,8 @@ class GuiDocEditor(QPlainTextEdit):
# Set default text margins
# Due to cursor visibility, a part of the margin must be
# allocated to the document itself. See issue #1112.
- cW = 2*self.cursorWidth()
- self._qDocument.setDocumentMargin(cW)
- self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
+ self._qDocument.setDocumentMargin(4)
+ self._vpMargin = max(CONFIG.getTextMargin() - 4, 0)
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
# Also set the document text options for the document text flow
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 64d1f34e..5acd7ef7 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -452,7 +452,7 @@ class TextBlockData(QTextBlockUserData):
while rxSpell.hasNext():
rxMatch = rxSpell.next()
if not SHARED.spelling.checkWord(rxMatch.captured(0)):
- if rxMatch.captured(0).isalpha() and not rxMatch.captured(0).isupper():
+ if not rxMatch.captured(0).isnumeric() and not rxMatch.captured(0).isupper():
self._spellErrors.append((rxMatch.capturedStart(0), rxMatch.capturedLength(0)))
return self._spellErrors
From b11a3c4bc777b8c0e0cf28daf2d1ce6c45a1c326 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 17 Oct 2023 20:54:00 +0200
Subject: [PATCH 14/15] Add editor spell checking test coverage
---
.../guiEditor_Main_Final_000000000000f.nwd | 6 +-
.../guiEditor_Main_Final_nwProject.nwx | 8 +-
tests/test_gui/test_gui_guimain.py | 292 ++++++++++--------
3 files changed, 169 insertions(+), 137 deletions(-)
diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
index 840d78eb..f5402f40 100644
--- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd
+++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
@@ -1,8 +1,8 @@
%%~name: New Scene
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
-%%~hash: fd5dc2f0c9767cb124b1bf2300d7a33b7780045e
-%%~date: 2023-08-25 18:08:01/2023-08-25 18:08:04
+%%~hash: 2a863dc53e09b0b1b0294ae7ea001853dddd596d
+%%~date: 2023-10-17 20:52:56/2023-10-17 20:53:01
# Novel
## Chapter
@@ -54,3 +54,5 @@ But don’t add a double space : See?
>>‘Right-aligned text’
+Some text with tesst in it.
+
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 104080e9..c56d9782 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
New Novel
Jane Doe
@@ -29,7 +29,7 @@
Main
-
+
-
Novel
@@ -47,7 +47,7 @@
New Chapter
-
-
+
New Scene
-
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 0a66ef13..70d47fe6 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -28,7 +28,7 @@ from tools import (
)
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QDialog, QMessageBox, QInputDialog
+from PyQt5.QtWidgets import QDialog, QMenu, QMessageBox, QInputDialog
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType, nwView, nwWidget
@@ -47,8 +47,7 @@ KEY_DELAY = 1
@pytest.mark.gui
def testGuiMain_ProjectBlocker(nwGUI):
- """Test the blocking of features when there's no project open.
- """
+ """Test the blocking of features when there's no project open."""
# Test no-project blocking
assert nwGUI.closeProject() is True
assert nwGUI.saveProject() is False
@@ -254,20 +253,25 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
+ # Text Editor
+ # ===========
+
+ docEditor: GuiDocEditor = nwGUI.docEditor
+
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
- qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Jane Doe":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@tag: Jane":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "This is a file about Jane.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Add a Plot File
nwGUI.switchFocus(nwWidget.TREE)
@@ -278,18 +282,18 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
- qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Main Plot":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@tag: MainPlot":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "This is a file detailing the main plot.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Add a World File
nwGUI.switchFocus(nwWidget.TREE)
@@ -299,24 +303,24 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert nwGUI.openSelectedItem()
# Add Some Text
- nwGUI.docEditor.replaceText("Hello World!")
- assert nwGUI.docEditor.getText() == "Hello World!"
- nwGUI.docEditor.replaceText("")
+ docEditor.replaceText("Hello World!")
+ assert docEditor.getText() == "Hello World!"
+ docEditor.replaceText("")
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
- qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Main Location":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@tag: Home":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "This is a file describing Jane's home.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Trigger autosaves before making more changes
nwGUI._autoSaveDocument()
@@ -332,67 +336,67 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
- qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Novel":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "## Chapter":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@pov: Jane":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@plot: MainPlot":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "### Scene":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "% How about a comment?":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@pov: Jane":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@plot: MainPlot":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@location: Home":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "#### Some Section":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "@char: Jane":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "This is a paragraph of nonsense text.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Don't allow Shift+Enter to insert a line separator (issue #1150)
for c in "This is another paragraph":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Enter, modifier=Qt.ShiftModifier, delay=KEY_DELAY)
for c in "with a line separator in it.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Auto-Replace
# ============
@@ -401,111 +405,137 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
"This is another paragraph of much longer nonsense text. "
"It is in fact 1 very very NONSENSICAL nonsense text! "
):
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
for c in "Isn't that nice? ":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
for c in "Ellipsis? Not a problem either ... ":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
for c in "How about three hyphens - -":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Left, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Right, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Left, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Right, delay=KEY_DELAY)
for c in "- for long dash? It works too.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "\"Full line double quoted text.\"":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "'Full line single quoted text.'":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Insert spaces before and after quotes
- nwGUI.docEditor._typPadBefore = "\u201d"
- nwGUI.docEditor._typPadAfter = "\u201c"
+ docEditor._typPadBefore = "\u201d"
+ docEditor._typPadAfter = "\u201c"
for c in "Some \"double quoted text with spaces padded\".":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
- nwGUI.docEditor._typPadBefore = ""
- nwGUI.docEditor._typPadAfter = ""
+ docEditor._typPadBefore = ""
+ docEditor._typPadAfter = ""
# Insert spaces before colon, but ignore tags and synopsis
- nwGUI.docEditor._typPadBefore = ":"
+ docEditor._typPadBefore = ":"
for c in "@object: NoSpaceAdded":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "% synopsis: No space before this colon.":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "Add space before this colon: See?":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "But don't add a double space : See?":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
- nwGUI.docEditor._typPadBefore = ""
+ docEditor._typPadBefore = ""
# Indent and Align
# ================
for c in "\t\"Tab-indented text\"":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in ">\"Paragraph-indented text\"":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in ">>\"Right-aligned text\"":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in "\t'Tab-indented text'":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in ">'Paragraph-indented text'":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
for c in ">>'Right-aligned text'":
- qtbot.keyClick(nwGUI.docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
- nwGUI.docEditor.wCounterDoc.run()
+ docEditor.wCounterDoc.run()
+
+ # Spell Checking
+ # ==============
+
+ for c in "Some text with tesst in it.":
+ qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+ qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+
+ currPos = docEditor.getCursorPosition()
+ assert docEditor._qDocument.spellErrorAtPos(currPos) == ("", -1, -1, [])
+
+ errPos = currPos - 13
+ word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos)
+ assert word == "tesst"
+ assert cPos == 15
+ assert cLen == 5
+ assert "test" in suggest
+
+ with monkeypatch.context() as mp:
+ mp.setattr(QMenu, "exec_", lambda *a: None)
+ docEditor.setCursorPosition(errPos)
+ docEditor._openSpellContext()
+
+ # Check Files
+ # ===========
# Save the document
- assert nwGUI.docEditor.docChanged
+ assert docEditor.docChanged
assert nwGUI.saveDocument()
- assert not nwGUI.docEditor.docChanged
+ assert not docEditor.docChanged
nwGUI.rebuildIndex()
# Open and view the edited document
From 90ca8f481dcfef9910823f40284938cdbdfd00a1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 17 Oct 2023 20:58:57 +0200
Subject: [PATCH 15/15] Disable checking spelling in tests on Windows
---
tests/test_gui/test_gui_guimain.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 70d47fe6..30a17c5b 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import sys
import pytest
from shutil import copyfile
@@ -518,11 +519,13 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert docEditor._qDocument.spellErrorAtPos(currPos) == ("", -1, -1, [])
errPos = currPos - 13
- word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos)
- assert word == "tesst"
- assert cPos == 15
- assert cLen == 5
- assert "test" in suggest
+ if not sys.platform.startswith("win32"):
+ # Skip on Windows as spell checking is off there
+ word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos)
+ assert word == "tesst"
+ assert cPos == 15
+ assert cLen == 5
+ assert "test" in suggest
with monkeypatch.context() as mp:
mp.setattr(QMenu, "exec_", lambda *a: None)