Add a document outline dropdown (#1764)

This commit is contained in:
Veronica Berglyd Olsen
2024-03-17 18:19:35 +01:00
committed by GitHub
13 changed files with 435 additions and 289 deletions
+9 -6
View File
@@ -519,11 +519,16 @@ class NWIndex:
def getItemHeading(self, tHandle: str, sTitle: str) -> IndexHeading | None: def getItemHeading(self, tHandle: str, sTitle: str) -> IndexHeading | None:
"""Get the heading entry for a specific item and heading.""" """Get the heading entry for a specific item and heading."""
tItem = self._itemIndex[tHandle] if tItem := self._itemIndex[tHandle]:
if isinstance(tItem, IndexItem):
return tItem[sTitle] return tItem[sTitle]
return None return None
def iterItemHeadings(self, tHandle: str) -> Iterator[str, IndexHeading]:
"""Get all headings for a specific item."""
if tItem := self._itemIndex[tHandle]:
yield from tItem.items()
return []
def novelStructure( def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True self, rootHandle: str | None = None, activeOnly: bool = True
) -> Iterator[tuple[str, str, str, IndexHeading]]: ) -> Iterator[tuple[str, str, str, IndexHeading]]:
@@ -900,13 +905,11 @@ class ItemIndex:
if rHandle is None: if rHandle is None:
for sTitle in self._items[tHandle].headings(): for sTitle in self._items[tHandle].headings():
hItem = self._items[tHandle][sTitle] if hItem := self._items[tHandle][sTitle]:
if hItem:
yield tHandle, sTitle, hItem yield tHandle, sTitle, hItem
elif tItem.itemRoot == rHandle: elif tItem.itemRoot == rHandle:
for sTitle in self._items[tHandle].headings(): for sTitle in self._items[tHandle].headings():
hItem = self._items[tHandle][sTitle] if hItem := self._items[tHandle][sTitle]:
if hItem:
yield tHandle, sTitle, hItem yield tHandle, sTitle, hItem
return return
+112 -69
View File
@@ -55,11 +55,11 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.common import minmax, transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst from novelwriter.constants import nwKeyWords, nwShortcode, nwUnicode
from novelwriter.tools.lipsum import GuiLipsum from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.core.document import NWDocument from novelwriter.core.document import NWDocument
from novelwriter.text.counting import standardCounter 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.gui.editordocument import GuiTextDocument
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -99,6 +99,7 @@ class GuiDocEditor(QPlainTextEdit):
toggleFocusModeRequest = pyqtSignal() toggleFocusModeRequest = pyqtSignal()
requestProjectItemSelected = pyqtSignal(str, bool) requestProjectItemSelected = pyqtSignal(str, bool)
requestProjectItemRenamed = pyqtSignal(str, str) requestProjectItemRenamed = pyqtSignal(str, str)
requestNewNoteCreation = pyqtSignal(str, nwItemClass)
def __init__(self, mainGui: GuiMain) -> None: def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -185,18 +186,18 @@ class GuiDocEditor(QPlainTextEdit):
self.followTag2.activated.connect(self._processTag) self.followTag2.activated.connect(self._processTag)
# Set Up Document Word Counter # Set Up Document Word Counter
self.wcTimerDoc = QTimer() self.timerDoc = QTimer(self)
self.wcTimerDoc.timeout.connect(self._runDocCounter) self.timerDoc.timeout.connect(self._runDocumentTasks)
self.wcTimerDoc.setInterval(5000) self.timerDoc.setInterval(5000)
self.wCounterDoc = BackgroundWordCounter(self) self.wCounterDoc = BackgroundWordCounter(self)
self.wCounterDoc.setAutoDelete(False) self.wCounterDoc.setAutoDelete(False)
self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts) self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
# Set Up Selection Word Counter # Set Up Selection Word Counter
self.wcTimerSel = QTimer() self.timerSel = QTimer(self)
self.wcTimerSel.timeout.connect(self._runSelCounter) self.timerSel.timeout.connect(self._runSelCounter)
self.wcTimerSel.setInterval(500) self.timerSel.setInterval(500)
self.wCounterSel = BackgroundWordCounter(self, forSelection=True) self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
self.wCounterSel.setAutoDelete(False) self.wCounterSel.setAutoDelete(False)
@@ -249,8 +250,8 @@ class GuiDocEditor(QPlainTextEdit):
self._nwDocument = None self._nwDocument = None
self.setReadOnly(True) self.setReadOnly(True)
self.clear() self.clear()
self.wcTimerDoc.stop() self.timerDoc.stop()
self.wcTimerSel.stop() self.timerSel.stop()
self._docHandle = None self._docHandle = None
self._lastEdit = 0.0 self._lastEdit = 0.0
@@ -259,7 +260,7 @@ class GuiDocEditor(QPlainTextEdit):
self._doReplace = False self._doReplace = False
self.setDocumentChanged(False) self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.clearHeader()
self.docFooter.setHandle(self._docHandle) self.docFooter.setHandle(self._docHandle)
self.docToolBar.setVisible(False) self.docToolBar.setVisible(False)
@@ -363,7 +364,7 @@ class GuiDocEditor(QPlainTextEdit):
# which makes it read only. # which makes it read only.
if self._docHandle: if self._docHandle:
self._qDocument.syntaxHighlighter.rehighlight() self._qDocument.syntaxHighlighter.rehighlight()
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setHandle(self._docHandle)
else: else:
self.clearEditor() self.clearEditor()
@@ -397,8 +398,8 @@ class GuiDocEditor(QPlainTextEdit):
self._lastEdit = time() self._lastEdit = time()
self._lastActive = time() self._lastActive = time()
self._runDocCounter() self._runDocumentTasks()
self.wcTimerDoc.start() self.timerDoc.start()
self.setReadOnly(False) self.setReadOnly(False)
self.updateDocMargins() self.updateDocMargins()
@@ -408,8 +409,8 @@ class GuiDocEditor(QPlainTextEdit):
elif isinstance(tLine, int): elif isinstance(tLine, int):
self.setCursorLine(tLine) self.setCursorLine(tLine)
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setHandle(tHandle)
self.docFooter.setHandle(self._docHandle) self.docFooter.setHandle(tHandle)
# This is a hack to fix invisible cursor on an empty document # This is a hack to fix invisible cursor on an empty document
if self._qDocument.characterCount() <= 1: if self._qDocument.characterCount() <= 1:
@@ -432,7 +433,7 @@ class GuiDocEditor(QPlainTextEdit):
def updateTagHighLighting(self) -> None: def updateTagHighLighting(self) -> None:
"""Rerun the syntax highlighter on all meta data lines.""" """Rerun the syntax highlighter on all meta data lines."""
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META) self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return return
def replaceText(self, text: str) -> None: def replaceText(self, text: str) -> None:
@@ -548,8 +549,8 @@ class GuiDocEditor(QPlainTextEdit):
sH = hBar.height() if hBar.isVisible() else 0 sH = hBar.height() if hBar.isVisible() else 0
tM = self._vpMargin tM = self._vpMargin
if CONFIG.textWidth > 0 or self.mainGui.isFocusMode: if CONFIG.textWidth > 0 or SHARED.focusMode:
tW = CONFIG.getTextWidth(self.mainGui.isFocusMode) tW = CONFIG.getTextWidth(SHARED.focusMode)
tM = max((wW - sW - tW)//2, self._vpMargin) tM = max((wW - sW - tW)//2, self._vpMargin)
tB = self.frameWidth() tB = self.frameWidth()
@@ -991,8 +992,8 @@ class GuiDocEditor(QPlainTextEdit):
"""Called when an item label is changed to check if the document """Called when an item label is changed to check if the document
title bar needs updating, title bar needs updating,
""" """
if tHandle == self._docHandle: if tHandle and tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setHandle(tHandle)
self.docFooter.updateInfo() self.docFooter.updateInfo()
self.updateDocMargins() self.updateDocMargins()
return return
@@ -1033,8 +1034,8 @@ class GuiDocEditor(QPlainTextEdit):
if not self._docChanged: if not self._docChanged:
self.setDocumentChanged(removed != 0 or added != 0) self.setDocumentChanged(removed != 0 or added != 0)
if not self.wcTimerDoc.isActive(): if not self.timerDoc.isActive():
self.wcTimerDoc.start() self.timerDoc.start()
if (block := self._qDocument.findBlock(pos)).isValid(): if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text() text = block.text()
@@ -1084,7 +1085,7 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu = QMenu(self) ctxMenu = QMenu(self)
ctxMenu.setObjectName("ContextMenu") ctxMenu.setObjectName("ContextMenu")
if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE: if pBlock.userState() == BLOCK_TITLE:
action = ctxMenu.addAction(self.tr("Set as Document Name")) action = ctxMenu.addAction(self.tr("Set as Document Name"))
action.triggered.connect(lambda: self._emitRenameItem(pBlock)) action.triggered.connect(lambda: self._emitRenameItem(pBlock))
@@ -1179,10 +1180,8 @@ class GuiDocEditor(QPlainTextEdit):
return return
@pyqtSlot() @pyqtSlot()
def _runDocCounter(self) -> None: def _runDocumentTasks(self) -> None:
"""Decide whether to run the word counter, or not due to """Run timer document tasks."""
inactivity.
"""
if self._docHandle is None: if self._docHandle is None:
return return
@@ -1193,6 +1192,10 @@ class GuiDocEditor(QPlainTextEdit):
if time() - self._lastEdit < 25.0: if time() - self._lastEdit < 25.0:
logger.debug("Running word counter") logger.debug("Running word counter")
SHARED.runInThreadPool(self.wCounterDoc) SHARED.runInThreadPool(self.wCounterDoc)
self.docHeader.setOutline({
block.blockNumber(): block.text()
for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
})
return return
@@ -1214,10 +1217,10 @@ class GuiDocEditor(QPlainTextEdit):
information to the footer, and start the selection word counter. information to the footer, and start the selection word counter.
""" """
if self.textCursor().hasSelection(): if self.textCursor().hasSelection():
if not self.wcTimerSel.isActive(): if not self.timerSel.isActive():
self.wcTimerSel.start() self.timerSel.start()
else: else:
self.wcTimerSel.stop() self.timerSel.stop()
self.docFooter.updateWordCount(0, False) self.docFooter.updateWordCount(0, False)
return return
@@ -1241,7 +1244,7 @@ class GuiDocEditor(QPlainTextEdit):
if self._docHandle and self._nwItem: if self._docHandle and self._nwItem:
logger.debug("User selected %d words", wCount) logger.debug("User selected %d words", wCount)
self.docFooter.updateWordCount(wCount, True) self.docFooter.updateWordCount(wCount, True)
self.wcTimerSel.stop() self.timerSel.stop()
return return
@pyqtSlot() @pyqtSlot()
@@ -1303,6 +1306,7 @@ class GuiDocEditor(QPlainTextEdit):
self._docHandle, wrapAround=self.docSearch.doLoop self._docHandle, wrapAround=self.docSearch.doLoop
) )
self.beginSearch() self.beginSearch()
self.setFocus()
return return
cursor = self.textCursor() cursor = self.textCursor()
@@ -1323,6 +1327,7 @@ class GuiDocEditor(QPlainTextEdit):
self._docHandle, wrapAround=self.docSearch.doLoop self._docHandle, wrapAround=self.docSearch.doLoop
) )
self.beginSearch() self.beginSearch()
self.setFocus()
return return
else: else:
resIdx = 0 if doLoop else maxIdx resIdx = 0 if doLoop else maxIdx
@@ -1856,7 +1861,10 @@ class GuiDocEditor(QPlainTextEdit):
if text.startswith("@") and self._docHandle: if text.startswith("@") and self._docHandle:
isGood, tBits, tPos = SHARED.project.index.scanThis(text) isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY: if (
not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY
or tBits[0] not in nwKeyWords.VALID_KEYS
):
return nwTrinary.NEUTRAL return nwTrinary.NEUTRAL
tag = "" tag = ""
@@ -1884,13 +1892,9 @@ class GuiDocEditor(QPlainTextEdit):
"Do you want to create a new project note for the tag '{0}'?" "Do you want to create a new project note for the tag '{0}'?"
).format(tag)): ).format(tag)):
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS) itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
if SHARED.mainGui.projView.createNewNote(tag, itemClass): self.requestNewNoteCreation.emit(tag, itemClass)
self._qDocument.syntaxHighlighter.rehighlightBlock(block) qApp.processEvents()
else: self._qDocument.syntaxHighlighter.rehighlightBlock(block)
SHARED.error(self.tr(
"Could not create note in a root folder for '{0}'. "
"If one doesn't exist, you must create one first."
).format(trConst(nwLabels.CLASS_NAME[itemClass])))
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
@@ -2704,11 +2708,9 @@ class GuiDocEditSearch(QFrame):
@pyqtSlot() @pyqtSlot()
def _doSearch(self) -> None: def _doSearch(self) -> None:
"""Call the search action function for the document editor.""" """Call the search action function for the document editor."""
modKey = qApp.keyboardModifiers() self.docEditor.findNext(goBack=(
if modKey == Qt.KeyboardModifier.ShiftModifier: qApp.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier)
self.docEditor.findNext(goBack=True) )
else:
self.docEditor.findNext()
return return
@pyqtSlot() @pyqtSlot()
@@ -2799,9 +2801,9 @@ class GuiDocEditHeader(QWidget):
logger.debug("Create: GuiDocEditHeader") logger.debug("Create: GuiDocEditHeader")
self.docEditor = docEditor self.docEditor = docEditor
self.mainGui = docEditor.mainGui
self._docHandle = None self._docHandle = None
self._docOutline: dict[int, str] = {}
fPx = int(0.9*SHARED.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
mPx = CONFIG.pxInt(8) mPx = CONFIG.pxInt(8)
@@ -2824,6 +2826,9 @@ class GuiDocEditHeader(QWidget):
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.itemTitle.setFont(lblFont) self.itemTitle.setFont(lblFont)
# Other Widgets
self.outlineMenu = QMenu(self)
# Buttons # Buttons
self.tbButton = QToolButton(self) self.tbButton = QToolButton(self)
self.tbButton.setContentsMargins(0, 0, 0, 0) self.tbButton.setContentsMargins(0, 0, 0, 0)
@@ -2834,6 +2839,16 @@ class GuiDocEditHeader(QWidget):
self.tbButton.setToolTip(self.tr("Toggle Tool Bar")) self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit()) self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
self.outlineButton = QToolButton(self)
self.outlineButton.setContentsMargins(0, 0, 0, 0)
self.outlineButton.setIconSize(iconSize)
self.outlineButton.setFixedSize(fPx, fPx)
self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu)
self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.searchButton = QToolButton(self) self.searchButton = QToolButton(self)
self.searchButton.setContentsMargins(0, 0, 0, 0) self.searchButton.setContentsMargins(0, 0, 0, 0)
self.searchButton.setIconSize(iconSize) self.searchButton.setIconSize(iconSize)
@@ -2865,14 +2880,19 @@ class GuiDocEditHeader(QWidget):
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
self.outerBox.setSpacing(hSp) self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.tbButton, 0) self.outerBox.addWidget(self.tbButton, 0)
self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.searchButton, 0) self.outerBox.addWidget(self.searchButton, 0)
self.outerBox.addWidget(self.itemTitle, 1) self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addSpacing(fPx + hSp)
self.outerBox.addWidget(self.minmaxButton, 0) self.outerBox.addWidget(self.minmaxButton, 0)
self.outerBox.addWidget(self.closeButton, 0) self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx) self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
# Other Signals
SHARED.focusModeChanged.connect(self._focusModeChanged)
# Fix Margins and Size # Fix Margins and Size
# This is needed for high DPI systems. See issue #499. # This is needed for high DPI systems. See issue #499.
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
@@ -2888,9 +2908,38 @@ class GuiDocEditHeader(QWidget):
# Methods # Methods
## ##
def clearHeader(self) -> None:
"""Clear the header."""
self._docHandle = None
self._docOutline = {}
self.itemTitle.setText("")
self.outlineMenu.clear()
self.tbButton.setVisible(False)
self.outlineButton.setVisible(False)
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
self.minmaxButton.setVisible(False)
return
def setOutline(self, data: dict[int, str]) -> None:
"""Set the document outline dataset."""
if data != self._docOutline:
tStart = time()
self.outlineMenu.clear()
for number, text in data.items():
action = self.outlineMenu.addAction(text)
action.triggered.connect(
lambda _, number=number: self._gotoBlock(number)
)
self._docOutline = data
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.tbButton.setIcon(SHARED.theme.getIcon("menu")) self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
self.outlineButton.setIcon(SHARED.theme.getIcon("list"))
self.searchButton.setIcon(SHARED.theme.getIcon("search")) self.searchButton.setIcon(SHARED.theme.getIcon("search"))
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(SHARED.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
@@ -2900,8 +2949,10 @@ class GuiDocEditHeader(QWidget):
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}" "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(colText.red(), colText.green(), colText.blue()) ).format(colText.red(), colText.green(), colText.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbButton.setStyleSheet(buttonStyle) self.tbButton.setStyleSheet(buttonStyle)
self.outlineButton.setStyleSheet(buttonStyleMenu)
self.searchButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle)
self.minmaxButton.setStyleSheet(buttonStyle) self.minmaxButton.setStyleSheet(buttonStyle)
self.closeButton.setStyleSheet(buttonStyle) self.closeButton.setStyleSheet(buttonStyle)
@@ -2924,18 +2975,11 @@ class GuiDocEditHeader(QWidget):
return return
def setTitleFromHandle(self, tHandle: str | None) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the document title from the handle, or alternatively, set """Set the document title from the handle, or alternatively, set
the whole document path within the project. the whole document path within the project.
""" """
self._docHandle = tHandle self._docHandle = tHandle
if tHandle is None:
self.itemTitle.setText("")
self.tbButton.setVisible(False)
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
self.minmaxButton.setVisible(False)
return
if CONFIG.showFullPath: if CONFIG.showFullPath:
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed( self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
@@ -2946,22 +2990,12 @@ class GuiDocEditHeader(QWidget):
self.tbButton.setVisible(True) self.tbButton.setVisible(True)
self.searchButton.setVisible(True) self.searchButton.setVisible(True)
self.outlineButton.setVisible(True)
self.closeButton.setVisible(True) self.closeButton.setVisible(True)
self.minmaxButton.setVisible(True) self.minmaxButton.setVisible(True)
return return
def updateFocusMode(self) -> None:
"""Update the minimise/maximise icon of the Focus Mode button.
This function is called by the GuiMain class via the
toggleFocusMode function and should not be activated directly.
"""
if self.mainGui.isFocusMode:
self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise"))
else:
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
return
## ##
# Private Slots # Private Slots
## ##
@@ -2969,11 +3003,20 @@ class GuiDocEditHeader(QWidget):
@pyqtSlot() @pyqtSlot()
def _closeDocument(self) -> None: def _closeDocument(self) -> None:
"""Trigger the close editor on the main window.""" """Trigger the close editor on the main window."""
self.clearHeader()
self.closeDocumentRequest.emit() self.closeDocumentRequest.emit()
self.tbButton.setVisible(False) return
self.searchButton.setVisible(False)
self.closeButton.setVisible(False) @pyqtSlot(int)
self.minmaxButton.setVisible(False) def _gotoBlock(self, blockNumber: int) -> None:
"""Move cursor to a specific heading."""
self.docEditor.setCursorLine(blockNumber + 1)
return
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
"""Update minimise/maximise icon of the Focus Mode button."""
self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise" if focusMode else "maximise"))
return return
## ##
+10 -10
View File
@@ -45,16 +45,16 @@ logger = logging.getLogger(__name__)
SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b") SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
BLOCK_TITLE = 4
class GuiDocHighlighter(QSyntaxHighlighter): class GuiDocHighlighter(QSyntaxHighlighter):
__slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles") __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: def __init__(self, document: QTextDocument) -> None:
super().__init__(document) super().__init__(document)
@@ -272,12 +272,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
is significantly faster than running the regex checks used for is significantly faster than running the regex checks used for
text paragraphs. text paragraphs.
""" """
self.setCurrentBlockState(self.BLOCK_NONE) self.setCurrentBlockState(BLOCK_NONE)
if self._tHandle is None or not text: if self._tHandle is None or not text:
return return
if text.startswith("@"): # Keywords and commands if text.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META) self.setCurrentBlockState(BLOCK_META)
index = SHARED.project.index index = SHARED.project.index
isValid, bits, pos = index.scanThis(text) isValid, bits, pos = index.scanThis(text)
isGood = index.checkThese(bits, self._tHandle) isGood = index.checkThese(bits, self._tHandle)
@@ -301,7 +301,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return return
elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "###! ", "#### ")): elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "###! ", "#### ")):
self.setCurrentBlockState(self.BLOCK_TITLE) self.setCurrentBlockState(BLOCK_TITLE)
if text.startswith("# "): # Heading 1 if text.startswith("# "): # Heading 1
self.setFormat(0, 1, self._hStyles["head1h"]) self.setFormat(0, 1, self._hStyles["head1h"])
@@ -332,7 +332,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(4, len(text), self._hStyles["header3"]) self.setFormat(4, len(text), self._hStyles["header3"])
elif text.startswith("%"): # Comments elif text.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT) self.setCurrentBlockState(BLOCK_TEXT)
cStyle, _, cPos = processComment(text) cStyle, _, cPos = processComment(text)
if cStyle == nwComment.PLAIN: if cStyle == nwComment.PLAIN:
self.setFormat(0, len(text), self._hStyles["hidden"]) self.setFormat(0, len(text), self._hStyles["hidden"])
@@ -357,7 +357,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return return
# Regular Text # Regular Text
self.setCurrentBlockState(self.BLOCK_TEXT) self.setCurrentBlockState(BLOCK_TEXT)
for rX, xFmt in self.rxRules: for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(text, 0) rxItt = rX.globalMatch(text, 0)
while rxItt.hasNext(): while rxItt.hasNext():
+102 -58
View File
@@ -29,7 +29,6 @@ from __future__ import annotations
import logging import logging
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl
from PyQt5.QtGui import ( from PyQt5.QtGui import (
@@ -44,13 +43,10 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,17 +54,16 @@ class GuiDocViewer(QTextBrowser):
documentLoaded = pyqtSignal(str) documentLoaded = pyqtSignal(str)
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
closeDocumentRequest = pyqtSignal()
reloadDocumentRequest = pyqtSignal()
togglePanelVisibility = pyqtSignal() togglePanelVisibility = pyqtSignal()
requestProjectItemSelected = pyqtSignal(str, bool) requestProjectItemSelected = pyqtSignal(str, bool)
def __init__(self, mainGui: GuiMain) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiDocViewer") logger.debug("Create: GuiDocViewer")
# Class Variables
self.mainGui = mainGui
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
@@ -128,7 +123,7 @@ class GuiDocViewer(QTextBrowser):
self.clear() self.clear()
self.setSearchPaths([""]) self.setSearchPaths([""])
self._docHandle = None self._docHandle = None
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.clearHeader()
return return
def updateTheme(self) -> None: def updateTheme(self) -> None:
@@ -184,8 +179,7 @@ class GuiDocViewer(QTextBrowser):
self.setTabStopDistance(CONFIG.getTabWidth()) self.setTabStopDistance(CONFIG.getTabWidth())
# If we have a document open, we should reload it in case the font changed # If we have a document open, we should reload it in case the font changed
if self._docHandle is not None: self.reloadText()
self.reloadText()
return return
@@ -239,7 +233,11 @@ class GuiDocViewer(QTextBrowser):
self._docHandle = tHandle self._docHandle = tHandle
SHARED.project.data.setLastHandle(tHandle, "viewer") SHARED.project.data.setLastHandle(tHandle, "viewer")
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setHandle(tHandle)
self.docHeader.setOutline({
sTitle: (hItem.title, nwHeaders.H_LEVEL.get(hItem.level, 0))
for sTitle, hItem in SHARED.project.index.iterItemHeadings(tHandle)
})
self.updateDocMargins() self.updateDocMargins()
# Since we change the content while it may still be rendering, we mark # Since we change the content while it may still be rendering, we mark
@@ -281,13 +279,6 @@ class GuiDocViewer(QTextBrowser):
return False return False
return True return True
def navigateTo(self, tAnchor: str) -> None:
"""Go to a specific #link in the document."""
if isinstance(tAnchor, str) and tAnchor.startswith("#"):
logger.debug("Moving to anchor '%s'", tAnchor)
self.setSource(QUrl(tAnchor))
return
def clearNavHistory(self) -> None: def clearNavHistory(self) -> None:
"""Clear the navigation history.""" """Clear the navigation history."""
self.docHistory.clear() self.docHistory.clear()
@@ -340,11 +331,19 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot(str) @pyqtSlot(str)
def updateDocInfo(self, tHandle: str) -> None: def updateDocInfo(self, tHandle: str) -> None:
"""Update the header title bar if needed.""" """Update the header title bar if needed."""
if tHandle == self._docHandle: if tHandle and tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setHandle(tHandle)
self.updateDocMargins() self.updateDocMargins()
return return
@pyqtSlot(str)
def navigateTo(self, anchor: str) -> None:
"""Go to a specific #link in the document."""
if isinstance(anchor, str) and anchor.startswith("#"):
logger.debug("Moving to anchor '%s'", anchor)
self.setSource(QUrl(anchor))
return
## ##
# Private Slots # Private Slots
## ##
@@ -626,35 +625,50 @@ class GuiDocViewHeader(QWidget):
logger.debug("Create: GuiDocViewHeader") logger.debug("Create: GuiDocViewHeader")
self.docViewer = docViewer self.docViewer = docViewer
self.mainGui = docViewer.mainGui
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
self._docOutline: dict[int, tuple[str, int]] = {}
fPx = int(0.9*SHARED.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
hSp = CONFIG.pxInt(6) hSp = CONFIG.pxInt(6)
mPx = CONFIG.pxInt(8)
iconSize = QSize(fPx, fPx)
# Main Widget Settings # Main Widget Settings
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
# Title Label # Title Label
self.docTitle = QLabel() self.itemTitle = QLabel()
self.docTitle.setText("") self.itemTitle.setText("")
self.docTitle.setIndent(0) self.itemTitle.setIndent(0)
self.docTitle.setMargin(0) self.itemTitle.setMargin(0)
self.docTitle.setContentsMargins(0, 0, 0, 0) self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.docTitle.setAutoFillBackground(True) self.itemTitle.setAutoFillBackground(True)
self.docTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.docTitle.setFixedHeight(fPx) self.itemTitle.setFixedHeight(fPx)
lblFont = self.docTitle.font() lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.docTitle.setFont(lblFont) self.itemTitle.setFont(lblFont)
# Other Widgets
self.outlineMenu = QMenu(self)
# Buttons # Buttons
self.outlineButton = QToolButton(self)
self.outlineButton.setContentsMargins(0, 0, 0, 0)
self.outlineButton.setIconSize(iconSize)
self.outlineButton.setFixedSize(fPx, fPx)
self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu)
self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.backButton = QToolButton(self) self.backButton = QToolButton(self)
self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setContentsMargins(0, 0, 0, 0)
self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setIconSize(iconSize)
self.backButton.setFixedSize(fPx, fPx) self.backButton.setFixedSize(fPx, fPx)
self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.backButton.setVisible(False) self.backButton.setVisible(False)
@@ -663,7 +677,7 @@ class GuiDocViewHeader(QWidget):
self.forwardButton = QToolButton(self) self.forwardButton = QToolButton(self)
self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setContentsMargins(0, 0, 0, 0)
self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setIconSize(iconSize)
self.forwardButton.setFixedSize(fPx, fPx) self.forwardButton.setFixedSize(fPx, fPx)
self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.forwardButton.setVisible(False) self.forwardButton.setVisible(False)
@@ -672,7 +686,7 @@ class GuiDocViewHeader(QWidget):
self.refreshButton = QToolButton(self) self.refreshButton = QToolButton(self)
self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setContentsMargins(0, 0, 0, 0)
self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setIconSize(iconSize)
self.refreshButton.setFixedSize(fPx, fPx) self.refreshButton.setFixedSize(fPx, fPx)
self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
@@ -681,7 +695,7 @@ class GuiDocViewHeader(QWidget):
self.closeButton = QToolButton(self) self.closeButton = QToolButton(self)
self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setIconSize(iconSize)
self.closeButton.setFixedSize(fPx, fPx) self.closeButton.setFixedSize(fPx, fPx)
self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
@@ -691,19 +705,21 @@ class GuiDocViewHeader(QWidget):
# Assemble Layout # Assemble Layout
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
self.outerBox.setSpacing(hSp) self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.backButton, 0) self.outerBox.addWidget(self.backButton, 0)
self.outerBox.addWidget(self.forwardButton, 0) self.outerBox.addWidget(self.forwardButton, 0)
self.outerBox.addWidget(self.docTitle, 1) self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addSpacing(fPx + hSp)
self.outerBox.addWidget(self.refreshButton, 0) self.outerBox.addWidget(self.refreshButton, 0)
self.outerBox.addWidget(self.closeButton, 0) self.outerBox.addWidget(self.closeButton, 0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
# Fix Margins and Size # Fix Margins and Size
# This is needed for high DPI systems. See issue #499. # This is needed for high DPI systems. See issue #499.
cM = CONFIG.pxInt(8)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(cM, cM, cM, cM) self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setMinimumHeight(fPx + 2*cM) self.setMinimumHeight(fPx + 2*mPx)
# Fix the Colours # Fix the Colours
self.updateTheme() self.updateTheme()
@@ -716,8 +732,42 @@ class GuiDocViewHeader(QWidget):
# Methods # Methods
## ##
def clearHeader(self) -> None:
"""Clear the header."""
self._docHandle = None
self._docOutline = {}
self.itemTitle.setText("")
self.outlineMenu.clear()
self.outlineButton.setVisible(False)
self.backButton.setVisible(False)
self.forwardButton.setVisible(False)
self.closeButton.setVisible(False)
self.refreshButton.setVisible(False)
return
def setOutline(self, data: dict[int, tuple[str, int]]) -> None:
"""Set the document outline dataset."""
if data != self._docOutline:
self.outlineMenu.clear()
entries = []
minLevel = 5
for title, (text, level) in data.items():
if title != "T0000":
entries.append((title, text, level))
minLevel = min(minLevel, level)
for title, text, level in entries[:30]:
indent = " "*(level - minLevel)
action = self.outlineMenu.addAction(f"{indent}{text}")
action.triggered.connect(
lambda _, title=title: self.docViewer.navigateTo(f"#{title}")
)
self._docOutline = data
return
def updateTheme(self) -> None: def updateTheme(self) -> None:
"""Update theme elements.""" """Update theme elements."""
self.outlineButton.setIcon(SHARED.theme.getIcon("list"))
self.backButton.setIcon(SHARED.theme.getIcon("backward")) self.backButton.setIcon(SHARED.theme.getIcon("backward"))
self.forwardButton.setIcon(SHARED.theme.getIcon("forward")) self.forwardButton.setIcon(SHARED.theme.getIcon("forward"))
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh")) self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
@@ -728,7 +778,9 @@ class GuiDocViewHeader(QWidget):
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}" "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(colText.red(), colText.green(), colText.blue()) ).format(colText.red(), colText.green(), colText.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.outlineButton.setStyleSheet(buttonStyleMenu)
self.backButton.setStyleSheet(buttonStyle) self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle)
self.refreshButton.setStyleSheet(buttonStyle) self.refreshButton.setStyleSheet(buttonStyle)
@@ -747,31 +799,25 @@ class GuiDocViewHeader(QWidget):
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette) self.setPalette(palette)
self.docTitle.setPalette(palette) self.itemTitle.setPalette(palette)
return return
def setTitleFromHandle(self, tHandle: str | None) -> None: def setHandle(self, tHandle: str) -> None:
"""Sets the document title from the handle, or alternatively, """Sets the document title from the handle, or alternatively,
set the whole document path. set the whole document path.
""" """
self._docHandle = tHandle self._docHandle = tHandle
if tHandle is None:
self.docTitle.setText("")
self.backButton.setVisible(False)
self.forwardButton.setVisible(False)
self.closeButton.setVisible(False)
self.refreshButton.setVisible(False)
return
if CONFIG.showFullPath: if CONFIG.showFullPath:
self.docTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed( self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
[name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)] [name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
))) )))
else: else:
self.docTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "") self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
self.backButton.setVisible(True) self.backButton.setVisible(True)
self.forwardButton.setVisible(True) self.forwardButton.setVisible(True)
self.outlineButton.setVisible(True)
self.closeButton.setVisible(True) self.closeButton.setVisible(True)
self.refreshButton.setVisible(True) self.refreshButton.setVisible(True)
@@ -790,15 +836,14 @@ class GuiDocViewHeader(QWidget):
@pyqtSlot() @pyqtSlot()
def _closeDocument(self) -> None: def _closeDocument(self) -> None:
"""Trigger the close editor/viewer on the main window.""" """Trigger the close editor/viewer on the main window."""
self.mainGui.closeDocViewer() self.clearHeader()
self.docViewer.closeDocumentRequest.emit()
return return
@pyqtSlot() @pyqtSlot()
def _refreshDocument(self) -> None: def _refreshDocument(self) -> None:
"""Reload the content of the document.""" """Reload the content of the document."""
if self.docViewer.docHandle == self.mainGui.docEditor.docHandle: self.docViewer.reloadDocumentRequest.emit()
self.mainGui.saveDocument()
self.docViewer.reloadText()
return return
## ##
@@ -829,7 +874,6 @@ class GuiDocViewFooter(QWidget):
logger.debug("Create: GuiDocViewFooter") logger.debug("Create: GuiDocViewFooter")
self.docViewer = docViewer self.docViewer = docViewer
self.mainGui = docViewer.mainGui
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
+12 -1
View File
@@ -23,11 +23,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
import logging import logging
from time import time 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.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp from PyQt5.QtWidgets import QPlainTextDocumentLayout, qApp
from novelwriter import SHARED from novelwriter import SHARED
@@ -113,6 +114,16 @@ class GuiTextDocument(QTextDocument):
return word, cPos, cLen, SHARED.spelling.suggestWords(word) return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, [] return "", -1, -1, []
def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Generator[QTextBlock]:
"""Iterate over all text blocks of a given type."""
count = 0
for i in range(self.blockCount()):
block = self.findBlockByNumber(i)
if count < maxCount and block.isValid() and block.userState() & cType > 0:
count += 1
yield block
return None
## ##
# Public Slots # Public Slots
## ##
+3 -3
View File
@@ -196,12 +196,12 @@ class GuiMainMenu(QMenuBar):
# Document > Open # Document > Open
self.aOpenDoc = self.docuMenu.addAction(self.tr("Open Document")) self.aOpenDoc = self.docuMenu.addAction(self.tr("Open Document"))
self.aOpenDoc.setShortcut("Ctrl+O") self.aOpenDoc.setShortcut("Ctrl+O")
self.aOpenDoc.triggered.connect(lambda: self.mainGui.openSelectedItem()) self.aOpenDoc.triggered.connect(self.mainGui.openSelectedItem)
# Document > Save # Document > Save
self.aSaveDoc = self.docuMenu.addAction(self.tr("Save Document")) self.aSaveDoc = self.docuMenu.addAction(self.tr("Save Document"))
self.aSaveDoc.setShortcut("Ctrl+S") self.aSaveDoc.setShortcut("Ctrl+S")
self.aSaveDoc.triggered.connect(lambda: self.mainGui.saveDocument()) self.aSaveDoc.triggered.connect(self.mainGui.saveDocument)
# Document > Close # Document > Close
self.aCloseDoc = self.docuMenu.addAction(self.tr("Close Document")) self.aCloseDoc = self.docuMenu.addAction(self.tr("Close Document"))
@@ -219,7 +219,7 @@ class GuiMainMenu(QMenuBar):
# Document > Close Preview # Document > Close Preview
self.aCloseView = self.docuMenu.addAction(self.tr("Close Document View")) self.aCloseView = self.docuMenu.addAction(self.tr("Close Document View"))
self.aCloseView.setShortcut("Ctrl+Shift+R") self.aCloseView.setShortcut("Ctrl+Shift+R")
self.aCloseView.triggered.connect(lambda: self.mainGui.closeDocViewer()) self.aCloseView.triggered.connect(self.mainGui.closeDocViewer)
# Document > Separator # Document > Separator
self.docuMenu.addSeparator() self.docuMenu.addSeparator()
+13 -8
View File
@@ -142,7 +142,6 @@ class GuiProjectView(QWidget):
self.requestDeleteItem = self.projTree.requestDeleteItem self.requestDeleteItem = self.projTree.requestDeleteItem
self.getSelectedHandle = self.projTree.getSelectedHandle self.getSelectedHandle = self.projTree.getSelectedHandle
self.changedSince = self.projTree.changedSince self.changedSince = self.projTree.changedSince
self.createNewNote = self.projTree.createNewNote
return return
@@ -240,6 +239,12 @@ class GuiProjectView(QWidget):
self.projBar.buildQuickLinksMenu() self.projBar.buildQuickLinksMenu()
return return
@pyqtSlot(str, nwItemClass)
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Process new not request."""
self.projTree.createNewNote(tag, itemClass)
return
# END Class GuiProjectView # END Class GuiProjectView
@@ -606,18 +611,18 @@ class GuiProjectTree(QTreeWidget):
self._timeChanged = 0.0 self._timeChanged = 0.0
return return
def createNewNote(self, tag: str, itemClass: nwItemClass | None) -> bool: def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Create a new note. This function is used by the document """Create a new note. This function is used by the document
editor to create note files for unknown tags. editor to create note files for unknown tags.
""" """
rHandle = SHARED.project.tree.findRoot(itemClass) if itemClass != nwItemClass.NO_CLASS:
if rHandle: if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
tHandle = SHARED.project.newFile(tag, rHandle) self.newTreeItem(nwItemType.ROOT, itemClass)
if tHandle: rHandle = SHARED.project.tree.findRoot(itemClass)
if rHandle and (tHandle := SHARED.project.newFile(tag, rHandle)):
SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n") SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self.revealNewTreeItem(tHandle, wordCount=True) self.revealNewTreeItem(tHandle, wordCount=True)
return True return
return False
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None, def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool: hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
+103 -101
View File
@@ -104,9 +104,6 @@ class GuiMain(QMainWindow):
# Initialise UserData Instance # Initialise UserData Instance
SHARED.initSharedData(self, GuiTheme()) SHARED.initSharedData(self, GuiTheme())
# Core Settings
self.isFocusMode = False
# Prepare Main Window # Prepare Main Window
self.resize(*CONFIG.mainWinSize) self.resize(*CONFIG.mainWinSize)
self._updateWindowTitle() self._updateWindowTitle()
@@ -233,6 +230,7 @@ class GuiMain(QMainWindow):
SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus) SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage) SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
SHARED.focusModeChanged.connect(self._focusModeChanged)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags) SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged) SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
SHARED.indexScannedText.connect(self.projView.updateItemValues) SHARED.indexScannedText.connect(self.projView.updateItemValues)
@@ -275,9 +273,12 @@ class GuiMain(QMainWindow):
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode) self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle) self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem) self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle) self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
self.docViewer.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
self.docViewer.reloadDocumentRequest.connect(self._reloadViewer)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility) self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle) self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
@@ -401,7 +402,7 @@ class GuiMain(QMainWindow):
if saveOK: if saveOK:
self.closeDocument() self.closeDocument()
self.docViewer.clearNavHistory() self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False) self.closeViewerPanel(byUser=False)
self.docViewerPanel.closeProjectTasks() self.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks() self.outlineView.closeProjectTasks()
@@ -523,24 +524,19 @@ class GuiMain(QMainWindow):
# Document Actions # Document Actions
## ##
def closeDocument(self, beforeOpen: bool = False) -> bool: def closeDocument(self, beforeOpen: bool = False) -> None:
"""Close the document and clear the editor and title field.""" """Close the document and clear the editor and title field."""
if not SHARED.hasProject: if SHARED.hasProject:
logger.error("No project open") # Disable focus mode if it is active
return False if SHARED.focusMode:
SHARED.setFocusMode(False)
# Disable focus mode if it is active self.docEditor.saveCursorPosition()
if self.isFocusMode: if self.docEditor.docChanged:
self.toggleFocusMode() self.saveDocument()
self.docEditor.clearEditor()
self.docEditor.saveCursorPosition() if not beforeOpen:
if self.docEditor.docChanged: self.novelView.setActiveHandle(None)
self.saveDocument() return
self.docEditor.clearEditor()
if not beforeOpen:
self.novelView.setActiveHandle(None)
return True
def openDocument(self, tHandle: str | None, tLine: int | None = None, def openDocument(self, tHandle: str | None, tLine: int | None = None,
changeFocus: bool = True, doScroll: bool = False) -> bool: changeFocus: bool = True, doScroll: bool = False) -> bool:
@@ -604,13 +600,12 @@ class GuiMain(QMainWindow):
return False return False
def saveDocument(self) -> bool: @pyqtSlot()
def saveDocument(self) -> None:
"""Save the current documents.""" """Save the current documents."""
if not SHARED.hasProject: if SHARED.hasProject:
logger.error("No project open") self.docEditor.saveText()
return False return
self.docEditor.saveText()
return True
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
"""Load a document for viewing in the view panel.""" """Load a document for viewing in the view panel."""
@@ -640,7 +635,8 @@ class GuiMain(QMainWindow):
self._changeView(nwView.EDITOR) self._changeView(nwView.EDITOR)
logger.debug("Viewing document with handle '%s'", tHandle) logger.debug("Viewing document with handle '%s'", tHandle)
if self.docViewer.loadText(tHandle): updateHistory = tHandle != self.docViewer.docHandle
if self.docViewer.loadText(tHandle, updateHistory=updateHistory):
if not self.splitView.isVisible(): if not self.splitView.isVisible():
cursorVisible = self.docEditor.cursorIsVisible() cursorVisible = self.docEditor.cursorIsVisible()
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
@@ -713,80 +709,71 @@ class GuiMain(QMainWindow):
# Tree Item Actions # Tree Item Actions
## ##
def openSelectedItem(self) -> bool: @pyqtSlot()
def openSelectedItem(self) -> None:
"""Open the selected item from the tree that is currently """Open the selected item from the tree that is currently
active. It is not checked that the item is actually a document. active. It is not checked that the item is actually a document.
That should be handled by the openDocument function. That should be handled by the openDocument function.
""" """
if not SHARED.hasProject: if SHARED.hasProject:
logger.error("No project open") tHandle = None
return False sTitle = None
tLine = None
if self.projView.treeHasFocus():
tHandle = self.projView.getSelectedHandle()
elif self.novelView.treeHasFocus():
tHandle, sTitle = self.novelView.getSelectedHandle()
elif self.outlineView.treeHasFocus():
tHandle, sTitle = self.outlineView.getSelectedHandle()
else:
logger.warning("No item selected")
return
tHandle = None if tHandle and sTitle:
sTitle = None if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle):
tLine = None tLine = hItem.line
if self.projView.treeHasFocus(): if tHandle:
tHandle = self.projView.getSelectedHandle() self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
elif self.novelView.treeHasFocus():
tHandle, sTitle = self.novelView.getSelectedHandle()
elif self.outlineView.treeHasFocus():
tHandle, sTitle = self.outlineView.getSelectedHandle()
else:
logger.warning("No item selected")
return False
if tHandle is not None and sTitle is not None: return
hItem = SHARED.project.index.getItemHeading(tHandle, sTitle)
if hItem is not None:
tLine = hItem.line
if tHandle is not None: def editItemLabel(self, tHandle: str | None = None) -> None:
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
return True
def editItemLabel(self, tHandle: str | None = None) -> bool:
"""Open the edit item dialog.""" """Open the edit item dialog."""
if not SHARED.hasProject: if SHARED.hasProject:
logger.error("No project open") if tHandle is None and (self.docEditor.anyFocus() or SHARED.focusMode):
return False tHandle = self.docEditor.docHandle
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): self.projView.renameTreeItem(tHandle)
tHandle = self.docEditor.docHandle return
self.projView.renameTreeItem(tHandle)
return True
def rebuildTrees(self) -> None: def rebuildTrees(self) -> None:
"""Rebuild the project tree.""" """Rebuild the project tree."""
self.projView.populateTree() self.projView.populateTree()
return return
def rebuildIndex(self, beQuiet: bool = False) -> bool: def rebuildIndex(self, beQuiet: bool = False) -> None:
"""Rebuild the entire index.""" """Rebuild the entire index."""
if not SHARED.hasProject: if SHARED.hasProject:
logger.error("No project open") logger.info("Rebuilding index ...")
return False qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
tStart = time()
logger.info("Rebuilding index ...") self.projView.saveProjectTasks()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) SHARED.project.index.rebuildIndex()
tStart = time() self.projView.populateTree()
self.novelView.refreshTree()
self.projView.saveProjectTasks() tEnd = time()
SHARED.project.index.rebuildIndex() self.mainStatus.setStatusMessage(
self.projView.populateTree() self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
self.novelView.refreshTree() )
self.docEditor.updateTagHighLighting()
self._updateStatusWordCount()
qApp.restoreOverrideCursor()
tEnd = time() if not beQuiet:
self.mainStatus.setStatusMessage( SHARED.info(self.tr("The project index has been successfully rebuilt."))
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
)
self.docEditor.updateTagHighLighting()
self._updateStatusWordCount()
qApp.restoreOverrideCursor()
if not beQuiet: return
SHARED.info(self.tr("The project index has been successfully rebuilt."))
return True
## ##
# Main Dialogs # Main Dialogs
@@ -896,15 +883,14 @@ class GuiMain(QMainWindow):
SHARED.error(self.tr("Could not initialise the dialog.")) SHARED.error(self.tr("Could not initialise the dialog."))
return return
def reportConfErr(self) -> bool: def reportConfErr(self) -> None:
"""Checks if the Config module has any errors to report, and let """Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches the user know if this is the case. The Config module caches
errors since it is initialised before the GUI itself. errors since it is initialised before the GUI itself.
""" """
if CONFIG.hasError: if CONFIG.hasError:
SHARED.error(CONFIG.errorText()) SHARED.error(CONFIG.errorText())
return True return
return False
## ##
# Main Window Actions # Main Window Actions
@@ -922,7 +908,7 @@ class GuiMain(QMainWindow):
logger.info("Exiting novelWriter") logger.info("Exiting novelWriter")
if not self.isFocusMode: if not SHARED.focusMode:
CONFIG.setMainPanePos(self.splitMain.sizes()) CONFIG.setMainPanePos(self.splitMain.sizes())
CONFIG.setOutlinePanePos(self.outlineView.splitSizes()) CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
if self.docViewerPanel.isVisible(): if self.docViewerPanel.isVisible():
@@ -943,7 +929,7 @@ class GuiMain(QMainWindow):
return True return True
def closeDocViewer(self, byUser: bool = True) -> bool: def closeViewerPanel(self, byUser: bool = True) -> bool:
"""Close the document view panel.""" """Close the document view panel."""
self.docViewer.clearViewer() self.docViewer.clearViewer()
if byUser: if byUser:
@@ -991,32 +977,40 @@ class GuiMain(QMainWindow):
SHARED.project.data.setLastHandle(None, "editor") SHARED.project.data.setLastHandle(None, "editor")
return return
@pyqtSlot()
def closeDocViewer(self) -> None:
"""Close the document viewer."""
self.closeViewerPanel()
SHARED.project.data.setLastHandle(None, "viewer")
return
@pyqtSlot() @pyqtSlot()
def toggleFocusMode(self) -> None: def toggleFocusMode(self) -> None:
"""Handle toggle focus mode. The Main GUI Focus Mode hides tree, """Toggle focus mode."""
if self.docEditor.docHandle:
SHARED.setFocusMode(not SHARED.focusMode)
return
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
"""Handle change of focus mode. The Main GUI Focus Mode hides tree,
view, statusbar and menu. view, statusbar and menu.
""" """
if self.docEditor.docHandle is None: if focusMode:
logger.error("No document open, so not activating Focus Mode")
return
self.isFocusMode = not self.isFocusMode
if self.isFocusMode:
logger.debug("Activating Focus Mode") logger.debug("Activating Focus Mode")
self.switchFocus(nwWidget.EDITOR) self.switchFocus(nwWidget.EDITOR)
else: else:
logger.debug("Deactivating Focus Mode") logger.debug("Deactivating Focus Mode")
cursorVisible = self.docEditor.cursorIsVisible() cursorVisible = self.docEditor.cursorIsVisible()
isVisible = not self.isFocusMode isVisible = not focusMode
self.treePane.setVisible(isVisible) self.treePane.setVisible(isVisible)
self.mainStatus.setVisible(isVisible) self.mainStatus.setVisible(isVisible)
self.mainMenu.setVisible(isVisible) self.mainMenu.setVisible(isVisible)
self.sideBar.setVisible(isVisible) self.sideBar.setVisible(isVisible)
hideDocFooter = self.isFocusMode and CONFIG.hideFocusFooter hideDocFooter = focusMode and CONFIG.hideFocusFooter
self.docEditor.docFooter.setVisible(not hideDocFooter) self.docEditor.docFooter.setVisible(not hideDocFooter)
self.docEditor.docHeader.updateFocusMode()
if self.splitView.isVisible(): if self.splitView.isVisible():
self.splitView.setVisible(False) self.splitView.setVisible(False)
@@ -1025,7 +1019,6 @@ class GuiMain(QMainWindow):
if cursorVisible: if cursorVisible:
self.docEditor.ensureCursorVisibleNoCentre() self.docEditor.ensureCursorVisibleNoCentre()
return return
@pyqtSlot(nwWidget) @pyqtSlot(nwWidget)
@@ -1154,6 +1147,15 @@ class GuiMain(QMainWindow):
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@pyqtSlot()
def _reloadViewer(self) -> None:
"""Reload the document in the viewer."""
if self.docEditor.docChanged and self.docEditor.docHandle == self.docViewer.docHandle:
# If the two panels have the same document, save any changes in the editor
self.saveDocument()
self.docViewer.reloadText()
return
@pyqtSlot(nwView) @pyqtSlot(nwView)
def _changeView(self, view: nwView) -> None: def _changeView(self, view: nwView) -> None:
"""Handle the requested change of view from the GuiViewBar.""" """Handle the requested change of view from the GuiViewBar."""
@@ -1266,8 +1268,8 @@ class GuiMain(QMainWindow):
"""Process escape keypress in the main window.""" """Process escape keypress in the main window."""
if self.docEditor.docSearch.isVisible(): if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch() self.docEditor.closeSearch()
elif self.isFocusMode: elif SHARED.focusMode:
self.toggleFocusMode() SHARED.setFocusMode(False)
return return
@pyqtSlot(int) @pyqtSlot(int)
+19
View File
@@ -57,6 +57,7 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool) projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str) projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str) spellLanguageChanged = pyqtSignal(str, str)
focusModeChanged = pyqtSignal(bool)
indexScannedText = pyqtSignal(str) indexScannedText = pyqtSignal(str)
indexChangedTags = pyqtSignal(list, list) indexChangedTags = pyqtSignal(list, list)
indexCleared = pyqtSignal() indexCleared = pyqtSignal()
@@ -76,6 +77,7 @@ class SharedData(QObject):
self._lastAlert = "" self._lastAlert = ""
self._idleTime = 0.0 self._idleTime = 0.0
self._idleRefTime = time() self._idleRefTime = time()
self._focusMode = False
return return
@@ -111,6 +113,11 @@ class SharedData(QObject):
raise Exception("SharedData class not fully initialised") raise Exception("SharedData class not fully initialised")
return self._spelling return self._spelling
@property
def focusMode(self) -> bool:
"""Return the Focus Mode state."""
return self._focusMode
@property @property
def hasProject(self) -> bool: def hasProject(self) -> bool:
"""Return True if the project instance is populated.""" """Return True if the project instance is populated."""
@@ -131,6 +138,17 @@ class SharedData(QObject):
"""Return the last alert message.""" """Return the last alert message."""
return self._lastAlert return self._lastAlert
##
# Setters
##
def setFocusMode(self, state: bool) -> None:
"""Set focus mode on or off."""
if state is not self._focusMode:
self._focusMode = state
self.focusModeChanged.emit(state)
return
## ##
# Methods # Methods
## ##
@@ -323,6 +341,7 @@ class SharedData(QObject):
self._project = NWProject() self._project = NWProject()
self._spelling = NWSpellEnchant(self._project) self._spelling = NWSpellEnchant(self._project)
self.updateSpellCheckLanguage() self.updateSpellCheckLanguage()
self._focusMode = False
return return
def _resetIdleTimer(self) -> None: def _resetIdleTimer(self) -> None:
+10
View File
@@ -705,6 +705,16 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
index.getItemHeading(dHandle, "T0001") index.getItemHeading(dHandle, "T0001")
)] )]
# getItemHeading
# ==============
assert list(index.iterItemHeadings(cHandle)) == [
("T0001", index.getItemHeading(cHandle, "T0001"))
]
assert list(index.iterItemHeadings(dHandle)) == [
("T0001", index.getItemHeading(dHandle, "T0001"))
]
assert list(index.iterItemHeadings(C.hInvalid)) == []
# getSingleTag # getSingleTag
# ============ # ============
assert index.getSingleTag("jane") == ( assert index.getSingleTag("jane") == (
+27 -14
View File
@@ -30,7 +30,9 @@ from PyQt5.QtCore import QThreadPool, Qt
from PyQt5.QtWidgets import QAction, QMenu, qApp from PyQt5.QtWidgets import QAction, QMenu, qApp
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout, nwTrinary, nwWidget from novelwriter.enum import (
nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary, nwWidget
)
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.gui.doceditor import GuiDocEditor, GuiDocToolBar from novelwriter.gui.doceditor import GuiDocEditor, GuiDocToolBar
from novelwriter.text.counting import standardCounter from novelwriter.text.counting import standardCounter
@@ -47,7 +49,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.openDocument(C.hSceneDoc) assert nwGUI.openDocument(C.hSceneDoc)
nwGUI.docEditor.setPlainText("### Lorem Ipsum\n\n%s" % ipsumText[0]) nwGUI.docEditor.setPlainText("### Lorem Ipsum\n\n%s" % ipsumText[0])
assert nwGUI.saveDocument() nwGUI.saveDocument()
# Check Defaults # Check Defaults
qDoc = nwGUI.docEditor.document() qDoc = nwGUI.docEditor.document()
@@ -58,6 +60,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.docEditor.docHeader.itemTitle.text() == ( assert nwGUI.docEditor.docHeader.itemTitle.text() == (
"Novel \u203a New Chapter \u203a New Scene" "Novel \u203a New Chapter \u203a New Scene"
) )
assert nwGUI.docEditor.docHeader._docOutline == {0: "### New Scene"}
# Check that editor handles settings # Check that editor handles settings
CONFIG.textFont = "" CONFIG.textFont = ""
@@ -84,6 +87,11 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Header # Header
# ====== # ======
# Go to outline
nwGUI.docEditor.setCursorLine(3)
nwGUI.docEditor.docHeader.outlineMenu.actions()[0].trigger()
assert nwGUI.docEditor.getCursorPosition() == 0
# Select item from header # Select item from header
with qtbot.waitSignal(nwGUI.docEditor.requestProjectItemSelected, timeout=1000) as signal: with qtbot.waitSignal(nwGUI.docEditor.requestProjectItemSelected, timeout=1000) as signal:
qtbot.mouseClick(nwGUI.docEditor.docHeader, Qt.MouseButton.LeftButton) qtbot.mouseClick(nwGUI.docEditor.docHeader, Qt.MouseButton.LeftButton)
@@ -116,8 +124,8 @@ def testGuiEditor_LoadText(qtbot, nwGUI, projPath, ipsumText, mockRnd):
longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20) longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20)
nwGUI.docEditor.replaceText(longText) nwGUI.docEditor.replaceText(longText)
assert nwGUI.saveDocument() is True nwGUI.saveDocument()
assert nwGUI.closeDocument() is True nwGUI.closeDocument()
# Invalid handle # Invalid handle
assert nwGUI.docEditor.loadText("abcdefghijklm") is False assert nwGUI.docEditor.loadText("abcdefghijklm") is False
@@ -132,7 +140,7 @@ def testGuiEditor_LoadText(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Load empty document # Load empty document
nwGUI.docEditor.replaceText("") nwGUI.docEditor.replaceText("")
assert nwGUI.saveDocument() is True nwGUI.saveDocument()
assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor.loadText(C.hSceneDoc) is True
assert nwGUI.docEditor.toPlainText() == "" assert nwGUI.docEditor.toPlainText() == ""
@@ -1478,7 +1486,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
cHandle = SHARED.project.newFile("Jane Doe", C.hCharRoot) cHandle = SHARED.project.newFile("Jane Doe", C.hCharRoot)
assert nwGUI.openDocument(cHandle) is True assert nwGUI.openDocument(cHandle) is True
nwGUI.docEditor.replaceText(text) nwGUI.docEditor.replaceText(text)
assert nwGUI.saveDocument() is True nwGUI.saveDocument()
assert nwGUI.projView.projTree.revealNewTreeItem(cHandle) assert nwGUI.projView.projTree.revealNewTreeItem(cHandle)
nwGUI.docEditor.updateTagHighLighting() nwGUI.docEditor.updateTagHighLighting()
@@ -1508,7 +1516,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.docViewer._docHandle is None assert nwGUI.docViewer._docHandle is None
assert nwGUI.docEditor._processTag(follow=True) is nwTrinary.POSITIVE assert nwGUI.docEditor._processTag(follow=True) is nwTrinary.POSITIVE
assert nwGUI.docViewer._docHandle == cHandle assert nwGUI.docViewer._docHandle == cHandle
assert nwGUI.closeDocViewer() is True assert nwGUI.closeViewerPanel() is True
assert nwGUI.docViewer._docHandle is None assert nwGUI.docViewer._docHandle is None
# On Unknown Tag, Create It # On Unknown Tag, Create It
@@ -1521,7 +1529,12 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert "0000000000012" not in SHARED.project.tree assert "0000000000012" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(42) nwGUI.docEditor.setCursorPosition(42)
assert nwGUI.docEditor._processTag(create=True) is nwTrinary.NEGATIVE assert nwGUI.docEditor._processTag(create=True) is nwTrinary.NEGATIVE
assert "0000000000012" not in SHARED.project.tree oHandle = SHARED.project.tree.findRoot(nwItemClass.OBJECT)
assert oHandle == "0000000000012"
oItem = SHARED.project.tree["0000000000013"]
assert oItem is not None
assert oItem.itemParent == "0000000000012"
nwGUI.docEditor.setCursorPosition(47) nwGUI.docEditor.setCursorPosition(47)
assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
@@ -1547,7 +1560,7 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
cHandle = SHARED.project.newFile("People", C.hCharRoot) cHandle = SHARED.project.newFile("People", C.hCharRoot)
assert nwGUI.openDocument(cHandle) is True assert nwGUI.openDocument(cHandle) is True
nwGUI.docEditor.replaceText(text) nwGUI.docEditor.replaceText(text)
assert nwGUI.saveDocument() is True nwGUI.saveDocument()
assert nwGUI.projView.projTree.revealNewTreeItem(cHandle) assert nwGUI.projView.projTree.revealNewTreeItem(cHandle)
docEditor = nwGUI.docEditor docEditor = nwGUI.docEditor
@@ -1686,13 +1699,13 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
threadPool = MockThreadPool() threadPool = MockThreadPool()
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool) monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
nwGUI.docEditor.wcTimerDoc.blockSignals(True) nwGUI.docEditor.timerDoc.blockSignals(True)
nwGUI.docEditor.wcTimerSel.blockSignals(True) nwGUI.docEditor.timerSel.blockSignals(True)
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
# Run on an empty document # Run on an empty document
nwGUI.docEditor._runDocCounter() nwGUI.docEditor._runDocumentTasks()
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
nwGUI.docEditor._updateDocCounts(0, 0, 0) nwGUI.docEditor._updateDocCounts(0, 0, 0)
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
@@ -1714,7 +1727,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
# Check that a busy counter is blocked # Check that a busy counter is blocked
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor.wCounterDoc, "isRunning", lambda *a: True) mp.setattr(nwGUI.docEditor.wCounterDoc, "isRunning", lambda *a: True)
nwGUI.docEditor._runDocCounter() nwGUI.docEditor._runDocumentTasks()
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -1723,7 +1736,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
# Run the full word counter # Run the full word counter
nwGUI.docEditor._runDocCounter() nwGUI.docEditor._runDocumentTasks()
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc) assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
nwGUI.docEditor.wCounterDoc.run() nwGUI.docEditor.wCounterDoc.run()
+2 -2
View File
@@ -185,12 +185,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
nwItem.setName("Test Title") # type: ignore nwItem.setName("Test Title") # type: ignore
assert nwItem.itemName == "Test Title" # type: ignore assert nwItem.itemName == "Test Title" # type: ignore
docViewer.updateDocInfo("4c4f28287af27") docViewer.updateDocInfo("4c4f28287af27")
assert docViewer.docHeader.docTitle.text() == "Characters \u203a Test Title" assert docViewer.docHeader.itemTitle.text() == "Characters \u203a Test Title"
# Title without full path # Title without full path
CONFIG.showFullPath = False CONFIG.showFullPath = False
docViewer.updateDocInfo("4c4f28287af27") docViewer.updateDocInfo("4c4f28287af27")
assert docViewer.docHeader.docTitle.text() == "Test Title" assert docViewer.docHeader.itemTitle.text() == "Test Title"
CONFIG.showFullPath = True CONFIG.showFullPath = True
# Document footer show/hide synopsis # Document footer show/hide synopsis
+13 -17
View File
@@ -49,15 +49,10 @@ def testGuiMain_ProjectBlocker(nwGUI):
# Test no-project blocking # Test no-project blocking
assert nwGUI.closeProject() is True assert nwGUI.closeProject() is True
assert nwGUI.saveProject() is False assert nwGUI.saveProject() is False
assert nwGUI.closeDocument() is False
assert nwGUI.openDocument(None) is False assert nwGUI.openDocument(None) is False
assert nwGUI.openNextDocument(None) is False assert nwGUI.openNextDocument(None) is False
assert nwGUI.saveDocument() is False
assert nwGUI.viewDocument(None) is False assert nwGUI.viewDocument(None) is False
assert nwGUI.importDocument() is False assert nwGUI.importDocument() is False
assert nwGUI.openSelectedItem() is False
assert nwGUI.editItemLabel() is False
assert nwGUI.rebuildIndex() is False
# END Test testGuiMain_ProjectBlocker # END Test testGuiMain_ProjectBlocker
@@ -109,7 +104,8 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
sHandle = "000000000000f" sHandle = "000000000000f"
assert nwGUI.openSelectedItem() is False nwGUI.openSelectedItem()
assert nwGUI.docEditor.docHandle is None
# Project Tree has focus # Project Tree has focus
nwGUI._changeView(nwView.PROJECT) nwGUI._changeView(nwView.PROJECT)
@@ -121,7 +117,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True nwGUI.closeDocument()
# Novel Tree has focus # Novel Tree has focus
nwGUI._changeView(nwView.NOVEL) nwGUI._changeView(nwView.NOVEL)
@@ -133,7 +129,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI.novelView.novelTree.setCurrentItem(selItem)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True nwGUI.closeDocument()
# Project Outline has focus # Project Outline has focus
nwGUI._changeView(nwView.OUTLINE) nwGUI._changeView(nwView.OUTLINE)
@@ -145,7 +141,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True nwGUI.closeDocument()
# qtbot.stop() # qtbot.stop()
@@ -238,7 +234,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True) nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem() nwGUI.openSelectedItem()
# Text Editor # Text Editor
# =========== # ===========
@@ -265,7 +261,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True) nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem() nwGUI.openSelectedItem()
# Type something into the document # Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR) nwGUI.switchFocus(nwWidget.EDITOR)
@@ -287,7 +283,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True) nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem() nwGUI.openSelectedItem()
# Add Some Text # Add Some Text
docEditor.replaceText("Hello World!") docEditor.replaceText("Hello World!")
@@ -319,7 +315,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True) nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True)
nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True) nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True)
nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True) nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True)
assert nwGUI.openSelectedItem() nwGUI.openSelectedItem()
# Type something into the document # Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR) nwGUI.switchFocus(nwWidget.EDITOR)
@@ -524,8 +520,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Save the document # Save the document
assert docEditor.docChanged assert docEditor.docChanged
assert nwGUI.saveDocument() nwGUI.saveDocument()
assert not docEditor.docChanged assert docEditor.docChanged is False
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
# Open and view the edited document # Open and view the edited document
@@ -533,7 +529,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert nwGUI.openDocument(C.hSceneDoc) assert nwGUI.openDocument(C.hSceneDoc)
assert nwGUI.viewDocument(C.hSceneDoc) assert nwGUI.viewDocument(C.hSceneDoc)
assert nwGUI.saveProject() assert nwGUI.saveProject()
assert nwGUI.closeDocViewer() assert nwGUI.closeViewerPanel()
# Check the files # Check the files
projFile = projPath / "nwProject.nwx" projFile = projPath / "nwProject.nwx"
@@ -575,7 +571,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
"""Test various features of the main window.""" """Test various features of the main window."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
assert nwGUI.isFocusMode is False assert SHARED.focusMode is False
# Focus Mode # Focus Mode
# ========== # ==========