Add document outline generator for editor

This commit is contained in:
Veronica Berglyd Olsen
2024-03-17 12:26:25 +01:00
parent c3aa8fec56
commit 5453d2ada1
3 changed files with 55 additions and 34 deletions
+35 -23
View File
@@ -59,7 +59,7 @@ from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode,
from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.core.document import NWDocument
from novelwriter.text.counting import standardCounter
from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -185,18 +185,18 @@ class GuiDocEditor(QPlainTextEdit):
self.followTag2.activated.connect(self._processTag)
# Set Up Document Word Counter
self.wcTimerDoc = QTimer()
self.wcTimerDoc.timeout.connect(self._runDocCounter)
self.wcTimerDoc.setInterval(5000)
self.timerDoc = QTimer(self)
self.timerDoc.timeout.connect(self._runDocumentTasks)
self.timerDoc.setInterval(5000)
self.wCounterDoc = BackgroundWordCounter(self)
self.wCounterDoc.setAutoDelete(False)
self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
# Set Up Selection Word Counter
self.wcTimerSel = QTimer()
self.wcTimerSel.timeout.connect(self._runSelCounter)
self.wcTimerSel.setInterval(500)
self.timerSel = QTimer(self)
self.timerSel.timeout.connect(self._runSelCounter)
self.timerSel.setInterval(500)
self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
self.wCounterSel.setAutoDelete(False)
@@ -249,8 +249,8 @@ class GuiDocEditor(QPlainTextEdit):
self._nwDocument = None
self.setReadOnly(True)
self.clear()
self.wcTimerDoc.stop()
self.wcTimerSel.stop()
self.timerDoc.stop()
self.timerSel.stop()
self._docHandle = None
self._lastEdit = 0.0
@@ -397,8 +397,8 @@ class GuiDocEditor(QPlainTextEdit):
self._lastEdit = time()
self._lastActive = time()
self._runDocCounter()
self.wcTimerDoc.start()
self._runDocumentTasks()
self.timerDoc.start()
self.setReadOnly(False)
self.updateDocMargins()
@@ -432,7 +432,7 @@ class GuiDocEditor(QPlainTextEdit):
def updateTagHighLighting(self) -> None:
"""Rerun the syntax highlighter on all meta data lines."""
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return
def replaceText(self, text: str) -> None:
@@ -1033,8 +1033,8 @@ class GuiDocEditor(QPlainTextEdit):
if not self._docChanged:
self.setDocumentChanged(removed != 0 or added != 0)
if not self.wcTimerDoc.isActive():
self.wcTimerDoc.start()
if not self.timerDoc.isActive():
self.timerDoc.start()
if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text()
@@ -1084,7 +1084,7 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu = QMenu(self)
ctxMenu.setObjectName("ContextMenu")
if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
if pBlock.userState() == BLOCK_TITLE:
action = ctxMenu.addAction(self.tr("Set as Document Name"))
action.triggered.connect(lambda: self._emitRenameItem(pBlock))
@@ -1179,10 +1179,8 @@ class GuiDocEditor(QPlainTextEdit):
return
@pyqtSlot()
def _runDocCounter(self) -> None:
"""Decide whether to run the word counter, or not due to
inactivity.
"""
def _runDocumentTasks(self) -> None:
"""Run timer document tasks."""
if self._docHandle is None:
return
@@ -1193,6 +1191,7 @@ class GuiDocEditor(QPlainTextEdit):
if time() - self._lastEdit < 25.0:
logger.debug("Running word counter")
SHARED.runInThreadPool(self.wCounterDoc)
self._updateOutline()
return
@@ -1214,10 +1213,10 @@ class GuiDocEditor(QPlainTextEdit):
information to the footer, and start the selection word counter.
"""
if self.textCursor().hasSelection():
if not self.wcTimerSel.isActive():
self.wcTimerSel.start()
if not self.timerSel.isActive():
self.timerSel.start()
else:
self.wcTimerSel.stop()
self.timerSel.stop()
self.docFooter.updateWordCount(0, False)
return
@@ -1241,7 +1240,7 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle and self._nwItem:
logger.debug("User selected %d words", wCount)
self.docFooter.updateWordCount(wCount, True)
self.wcTimerSel.stop()
self.timerSel.stop()
return
@pyqtSlot()
@@ -1837,6 +1836,13 @@ class GuiDocEditor(QPlainTextEdit):
# Internal Functions
##
def _updateOutline(self) -> None:
"""Scan the text for headings and update the outline."""
data = [(b.blockNumber(), b.text()) for b in self._qDocument.iterBlockByType(BLOCK_TITLE)]
logger.debug("Document contains %d heading(s)", len(data))
self.docHeader.setOutline(data)
return
def _processTag(self, cursor: QTextCursor | None = None,
follow: bool = True, create: bool = False) -> nwTrinary:
"""Activated by Ctrl+Enter. Checks that we're in a block
@@ -2802,6 +2808,7 @@ class GuiDocEditHeader(QWidget):
self.mainGui = docEditor.mainGui
self._docHandle = None
self._docOutline: list[tuple[int, str]] = []
fPx = int(0.9*SHARED.theme.fontPixelSize)
mPx = CONFIG.pxInt(8)
@@ -2888,6 +2895,11 @@ class GuiDocEditHeader(QWidget):
# Methods
##
def setOutline(self, data: list[tuple[int, str]]) -> None:
"""Set the document outline dataset."""
self._docOutline = data
return
def updateTheme(self) -> None:
"""Update theme elements."""
self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
+10 -10
View File
@@ -45,16 +45,16 @@ logger = logging.getLogger(__name__)
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
BLOCK_TITLE = 4
class GuiDocHighlighter(QSyntaxHighlighter):
__slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles")
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
BLOCK_TITLE = 4
def __init__(self, document: QTextDocument) -> None:
super().__init__(document)
@@ -272,12 +272,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
is significantly faster than running the regex checks used for
text paragraphs.
"""
self.setCurrentBlockState(self.BLOCK_NONE)
self.setCurrentBlockState(BLOCK_NONE)
if self._tHandle is None or not text:
return
if text.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
self.setCurrentBlockState(BLOCK_META)
index = SHARED.project.index
isValid, bits, pos = index.scanThis(text)
isGood = index.checkThese(bits, self._tHandle)
@@ -301,7 +301,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "###! ", "#### ")):
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setCurrentBlockState(BLOCK_TITLE)
if text.startswith("# "): # Heading 1
self.setFormat(0, 1, self._hStyles["head1h"])
@@ -332,7 +332,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(4, len(text), self._hStyles["header3"])
elif text.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
self.setCurrentBlockState(BLOCK_TEXT)
cStyle, _, cPos = processComment(text)
if cStyle == nwComment.PLAIN:
self.setFormat(0, len(text), self._hStyles["hidden"])
@@ -357,7 +357,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
# Regular Text
self.setCurrentBlockState(self.BLOCK_TEXT)
self.setCurrentBlockState(BLOCK_TEXT)
for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(text, 0)
while rxItt.hasNext():
+10 -1
View File
@@ -23,11 +23,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from collections.abc import Generator
import logging
from time import time
from PyQt5.QtGui import QTextCursor, QTextDocument
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextDocument
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp
from novelwriter import SHARED
@@ -113,6 +114,14 @@ class GuiTextDocument(QTextDocument):
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, []
def iterBlockByType(self, cType: int) -> Generator[QTextBlock]:
"""Iterate over all text blocks of a given type."""
for i in range(self.blockCount()):
block = self.findBlockByNumber(i)
if block.isValid() and block.userState() & cType > 0:
yield block
return None
##
# Public Slots
##