Add document outline generator for editor
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
##
|
||||
|
||||
Reference in New Issue
Block a user