Add a document outline dropdown (#1764)
This commit is contained in:
@@ -519,11 +519,16 @@ class NWIndex:
|
||||
|
||||
def getItemHeading(self, tHandle: str, sTitle: str) -> IndexHeading | None:
|
||||
"""Get the heading entry for a specific item and heading."""
|
||||
tItem = self._itemIndex[tHandle]
|
||||
if isinstance(tItem, IndexItem):
|
||||
if tItem := self._itemIndex[tHandle]:
|
||||
return tItem[sTitle]
|
||||
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(
|
||||
self, rootHandle: str | None = None, activeOnly: bool = True
|
||||
) -> Iterator[tuple[str, str, str, IndexHeading]]:
|
||||
@@ -900,13 +905,11 @@ class ItemIndex:
|
||||
|
||||
if rHandle is None:
|
||||
for sTitle in self._items[tHandle].headings():
|
||||
hItem = self._items[tHandle][sTitle]
|
||||
if hItem:
|
||||
if hItem := self._items[tHandle][sTitle]:
|
||||
yield tHandle, sTitle, hItem
|
||||
elif tItem.itemRoot == rHandle:
|
||||
for sTitle in self._items[tHandle].headings():
|
||||
hItem = self._items[tHandle][sTitle]
|
||||
if hItem:
|
||||
if hItem := self._items[tHandle][sTitle]:
|
||||
yield tHandle, sTitle, hItem
|
||||
|
||||
return
|
||||
|
||||
+112
-69
@@ -55,11 +55,11 @@ from PyQt5.QtWidgets import (
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
|
||||
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.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
|
||||
|
||||
@@ -99,6 +99,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
toggleFocusModeRequest = pyqtSignal()
|
||||
requestProjectItemSelected = pyqtSignal(str, bool)
|
||||
requestProjectItemRenamed = pyqtSignal(str, str)
|
||||
requestNewNoteCreation = pyqtSignal(str, nwItemClass)
|
||||
|
||||
def __init__(self, mainGui: GuiMain) -> None:
|
||||
super().__init__(parent=mainGui)
|
||||
@@ -185,18 +186,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 +250,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
|
||||
@@ -259,7 +260,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._doReplace = False
|
||||
|
||||
self.setDocumentChanged(False)
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.docHeader.clearHeader()
|
||||
self.docFooter.setHandle(self._docHandle)
|
||||
self.docToolBar.setVisible(False)
|
||||
|
||||
@@ -363,7 +364,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
# which makes it read only.
|
||||
if self._docHandle:
|
||||
self._qDocument.syntaxHighlighter.rehighlight()
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.docHeader.setHandle(self._docHandle)
|
||||
else:
|
||||
self.clearEditor()
|
||||
|
||||
@@ -397,8 +398,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()
|
||||
@@ -408,8 +409,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
elif isinstance(tLine, int):
|
||||
self.setCursorLine(tLine)
|
||||
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.docFooter.setHandle(self._docHandle)
|
||||
self.docHeader.setHandle(tHandle)
|
||||
self.docFooter.setHandle(tHandle)
|
||||
|
||||
# This is a hack to fix invisible cursor on an empty document
|
||||
if self._qDocument.characterCount() <= 1:
|
||||
@@ -432,7 +433,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:
|
||||
@@ -548,8 +549,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
sH = hBar.height() if hBar.isVisible() else 0
|
||||
|
||||
tM = self._vpMargin
|
||||
if CONFIG.textWidth > 0 or self.mainGui.isFocusMode:
|
||||
tW = CONFIG.getTextWidth(self.mainGui.isFocusMode)
|
||||
if CONFIG.textWidth > 0 or SHARED.focusMode:
|
||||
tW = CONFIG.getTextWidth(SHARED.focusMode)
|
||||
tM = max((wW - sW - tW)//2, self._vpMargin)
|
||||
|
||||
tB = self.frameWidth()
|
||||
@@ -991,8 +992,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
"""Called when an item label is changed to check if the document
|
||||
title bar needs updating,
|
||||
"""
|
||||
if tHandle == self._docHandle:
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
if tHandle and tHandle == self._docHandle:
|
||||
self.docHeader.setHandle(tHandle)
|
||||
self.docFooter.updateInfo()
|
||||
self.updateDocMargins()
|
||||
return
|
||||
@@ -1033,8 +1034,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 +1085,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 +1180,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 +1192,10 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if time() - self._lastEdit < 25.0:
|
||||
logger.debug("Running word counter")
|
||||
SHARED.runInThreadPool(self.wCounterDoc)
|
||||
self.docHeader.setOutline({
|
||||
block.blockNumber(): block.text()
|
||||
for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
@@ -1214,10 +1217,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 +1244,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()
|
||||
@@ -1303,6 +1306,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._docHandle, wrapAround=self.docSearch.doLoop
|
||||
)
|
||||
self.beginSearch()
|
||||
self.setFocus()
|
||||
return
|
||||
|
||||
cursor = self.textCursor()
|
||||
@@ -1323,6 +1327,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._docHandle, wrapAround=self.docSearch.doLoop
|
||||
)
|
||||
self.beginSearch()
|
||||
self.setFocus()
|
||||
return
|
||||
else:
|
||||
resIdx = 0 if doLoop else maxIdx
|
||||
@@ -1856,7 +1861,10 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if text.startswith("@") and self._docHandle:
|
||||
|
||||
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
|
||||
|
||||
tag = ""
|
||||
@@ -1884,13 +1892,9 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
"Do you want to create a new project note for the tag '{0}'?"
|
||||
).format(tag)):
|
||||
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
|
||||
if SHARED.mainGui.projView.createNewNote(tag, itemClass):
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
||||
else:
|
||||
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])))
|
||||
self.requestNewNoteCreation.emit(tag, itemClass)
|
||||
qApp.processEvents()
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
||||
|
||||
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
|
||||
|
||||
@@ -2704,11 +2708,9 @@ class GuiDocEditSearch(QFrame):
|
||||
@pyqtSlot()
|
||||
def _doSearch(self) -> None:
|
||||
"""Call the search action function for the document editor."""
|
||||
modKey = qApp.keyboardModifiers()
|
||||
if modKey == Qt.KeyboardModifier.ShiftModifier:
|
||||
self.docEditor.findNext(goBack=True)
|
||||
else:
|
||||
self.docEditor.findNext()
|
||||
self.docEditor.findNext(goBack=(
|
||||
qApp.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier)
|
||||
)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -2799,9 +2801,9 @@ class GuiDocEditHeader(QWidget):
|
||||
logger.debug("Create: GuiDocEditHeader")
|
||||
|
||||
self.docEditor = docEditor
|
||||
self.mainGui = docEditor.mainGui
|
||||
|
||||
self._docHandle = None
|
||||
self._docOutline: dict[int, str] = {}
|
||||
|
||||
fPx = int(0.9*SHARED.theme.fontPixelSize)
|
||||
mPx = CONFIG.pxInt(8)
|
||||
@@ -2824,6 +2826,9 @@ class GuiDocEditHeader(QWidget):
|
||||
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
|
||||
self.itemTitle.setFont(lblFont)
|
||||
|
||||
# Other Widgets
|
||||
self.outlineMenu = QMenu(self)
|
||||
|
||||
# Buttons
|
||||
self.tbButton = QToolButton(self)
|
||||
self.tbButton.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -2834,6 +2839,16 @@ class GuiDocEditHeader(QWidget):
|
||||
self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
|
||||
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.setContentsMargins(0, 0, 0, 0)
|
||||
self.searchButton.setIconSize(iconSize)
|
||||
@@ -2865,14 +2880,19 @@ class GuiDocEditHeader(QWidget):
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.outerBox.setSpacing(hSp)
|
||||
self.outerBox.addWidget(self.tbButton, 0)
|
||||
self.outerBox.addWidget(self.outlineButton, 0)
|
||||
self.outerBox.addWidget(self.searchButton, 0)
|
||||
self.outerBox.addWidget(self.itemTitle, 1)
|
||||
self.outerBox.addSpacing(fPx + hSp)
|
||||
self.outerBox.addWidget(self.minmaxButton, 0)
|
||||
self.outerBox.addWidget(self.closeButton, 0)
|
||||
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
# Other Signals
|
||||
SHARED.focusModeChanged.connect(self._focusModeChanged)
|
||||
|
||||
# Fix Margins and Size
|
||||
# This is needed for high DPI systems. See issue #499.
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -2888,9 +2908,38 @@ class GuiDocEditHeader(QWidget):
|
||||
# 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:
|
||||
"""Update theme elements."""
|
||||
self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
|
||||
self.outlineButton.setIcon(SHARED.theme.getIcon("list"))
|
||||
self.searchButton.setIcon(SHARED.theme.getIcon("search"))
|
||||
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
|
||||
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
|
||||
@@ -2900,8 +2949,10 @@ class GuiDocEditHeader(QWidget):
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
|
||||
).format(colText.red(), colText.green(), colText.blue())
|
||||
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
|
||||
|
||||
self.tbButton.setStyleSheet(buttonStyle)
|
||||
self.outlineButton.setStyleSheet(buttonStyleMenu)
|
||||
self.searchButton.setStyleSheet(buttonStyle)
|
||||
self.minmaxButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
@@ -2924,18 +2975,11 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
return
|
||||
|
||||
def setTitleFromHandle(self, tHandle: str | None) -> None:
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Set the document title from the handle, or alternatively, set
|
||||
the whole document path within the project.
|
||||
"""
|
||||
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:
|
||||
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
|
||||
@@ -2946,22 +2990,12 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
self.tbButton.setVisible(True)
|
||||
self.searchButton.setVisible(True)
|
||||
self.outlineButton.setVisible(True)
|
||||
self.closeButton.setVisible(True)
|
||||
self.minmaxButton.setVisible(True)
|
||||
|
||||
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
|
||||
##
|
||||
@@ -2969,11 +3003,20 @@ class GuiDocEditHeader(QWidget):
|
||||
@pyqtSlot()
|
||||
def _closeDocument(self) -> None:
|
||||
"""Trigger the close editor on the main window."""
|
||||
self.clearHeader()
|
||||
self.closeDocumentRequest.emit()
|
||||
self.tbButton.setVisible(False)
|
||||
self.searchButton.setVisible(False)
|
||||
self.closeButton.setVisible(False)
|
||||
self.minmaxButton.setVisible(False)
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
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
|
||||
|
||||
##
|
||||
|
||||
@@ -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():
|
||||
|
||||
+102
-58
@@ -29,7 +29,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl
|
||||
from PyQt5.QtGui import (
|
||||
@@ -44,13 +43,10 @@ from PyQt5.QtWidgets import (
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.constants import nwHeaders, nwUnicode
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
from novelwriter.extensions.eventfilters import WheelEventFilter
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.guimain import GuiMain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -58,17 +54,16 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
documentLoaded = pyqtSignal(str)
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
closeDocumentRequest = pyqtSignal()
|
||||
reloadDocumentRequest = pyqtSignal()
|
||||
togglePanelVisibility = pyqtSignal()
|
||||
requestProjectItemSelected = pyqtSignal(str, bool)
|
||||
|
||||
def __init__(self, mainGui: GuiMain) -> None:
|
||||
super().__init__(parent=mainGui)
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
logger.debug("Create: GuiDocViewer")
|
||||
|
||||
# Class Variables
|
||||
self.mainGui = mainGui
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
|
||||
@@ -128,7 +123,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.clear()
|
||||
self.setSearchPaths([""])
|
||||
self._docHandle = None
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.docHeader.clearHeader()
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
@@ -184,8 +179,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.setTabStopDistance(CONFIG.getTabWidth())
|
||||
|
||||
# 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
|
||||
|
||||
@@ -239,7 +233,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
self._docHandle = tHandle
|
||||
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()
|
||||
|
||||
# Since we change the content while it may still be rendering, we mark
|
||||
@@ -281,13 +279,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
return False
|
||||
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:
|
||||
"""Clear the navigation history."""
|
||||
self.docHistory.clear()
|
||||
@@ -340,11 +331,19 @@ class GuiDocViewer(QTextBrowser):
|
||||
@pyqtSlot(str)
|
||||
def updateDocInfo(self, tHandle: str) -> None:
|
||||
"""Update the header title bar if needed."""
|
||||
if tHandle == self._docHandle:
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
if tHandle and tHandle == self._docHandle:
|
||||
self.docHeader.setHandle(tHandle)
|
||||
self.updateDocMargins()
|
||||
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
|
||||
##
|
||||
@@ -626,35 +625,50 @@ class GuiDocViewHeader(QWidget):
|
||||
logger.debug("Create: GuiDocViewHeader")
|
||||
|
||||
self.docViewer = docViewer
|
||||
self.mainGui = docViewer.mainGui
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
self._docOutline: dict[int, tuple[str, int]] = {}
|
||||
|
||||
fPx = int(0.9*SHARED.theme.fontPixelSize)
|
||||
hSp = CONFIG.pxInt(6)
|
||||
mPx = CONFIG.pxInt(8)
|
||||
iconSize = QSize(fPx, fPx)
|
||||
|
||||
# Main Widget Settings
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
# Title Label
|
||||
self.docTitle = QLabel()
|
||||
self.docTitle.setText("")
|
||||
self.docTitle.setIndent(0)
|
||||
self.docTitle.setMargin(0)
|
||||
self.docTitle.setContentsMargins(0, 0, 0, 0)
|
||||
self.docTitle.setAutoFillBackground(True)
|
||||
self.docTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
|
||||
self.docTitle.setFixedHeight(fPx)
|
||||
self.itemTitle = QLabel()
|
||||
self.itemTitle.setText("")
|
||||
self.itemTitle.setIndent(0)
|
||||
self.itemTitle.setMargin(0)
|
||||
self.itemTitle.setContentsMargins(0, 0, 0, 0)
|
||||
self.itemTitle.setAutoFillBackground(True)
|
||||
self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
|
||||
self.itemTitle.setFixedHeight(fPx)
|
||||
|
||||
lblFont = self.docTitle.font()
|
||||
lblFont = self.itemTitle.font()
|
||||
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
|
||||
self.docTitle.setFont(lblFont)
|
||||
self.itemTitle.setFont(lblFont)
|
||||
|
||||
# Other Widgets
|
||||
self.outlineMenu = QMenu(self)
|
||||
|
||||
# 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.setContentsMargins(0, 0, 0, 0)
|
||||
self.backButton.setIconSize(QSize(fPx, fPx))
|
||||
self.backButton.setIconSize(iconSize)
|
||||
self.backButton.setFixedSize(fPx, fPx)
|
||||
self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.backButton.setVisible(False)
|
||||
@@ -663,7 +677,7 @@ class GuiDocViewHeader(QWidget):
|
||||
|
||||
self.forwardButton = QToolButton(self)
|
||||
self.forwardButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.forwardButton.setIconSize(QSize(fPx, fPx))
|
||||
self.forwardButton.setIconSize(iconSize)
|
||||
self.forwardButton.setFixedSize(fPx, fPx)
|
||||
self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.forwardButton.setVisible(False)
|
||||
@@ -672,7 +686,7 @@ class GuiDocViewHeader(QWidget):
|
||||
|
||||
self.refreshButton = QToolButton(self)
|
||||
self.refreshButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.refreshButton.setIconSize(QSize(fPx, fPx))
|
||||
self.refreshButton.setIconSize(iconSize)
|
||||
self.refreshButton.setFixedSize(fPx, fPx)
|
||||
self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.refreshButton.setVisible(False)
|
||||
@@ -681,7 +695,7 @@ class GuiDocViewHeader(QWidget):
|
||||
|
||||
self.closeButton = QToolButton(self)
|
||||
self.closeButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.closeButton.setIconSize(QSize(fPx, fPx))
|
||||
self.closeButton.setIconSize(iconSize)
|
||||
self.closeButton.setFixedSize(fPx, fPx)
|
||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.closeButton.setVisible(False)
|
||||
@@ -691,19 +705,21 @@ class GuiDocViewHeader(QWidget):
|
||||
# Assemble Layout
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.outerBox.setSpacing(hSp)
|
||||
self.outerBox.addWidget(self.outlineButton, 0)
|
||||
self.outerBox.addWidget(self.backButton, 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.closeButton, 0)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
# Fix Margins and Size
|
||||
# This is needed for high DPI systems. See issue #499.
|
||||
cM = CONFIG.pxInt(8)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.outerBox.setContentsMargins(cM, cM, cM, cM)
|
||||
self.setMinimumHeight(fPx + 2*cM)
|
||||
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
|
||||
self.setMinimumHeight(fPx + 2*mPx)
|
||||
|
||||
# Fix the Colours
|
||||
self.updateTheme()
|
||||
@@ -716,8 +732,42 @@ class GuiDocViewHeader(QWidget):
|
||||
# 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:
|
||||
"""Update theme elements."""
|
||||
self.outlineButton.setIcon(SHARED.theme.getIcon("list"))
|
||||
self.backButton.setIcon(SHARED.theme.getIcon("backward"))
|
||||
self.forwardButton.setIcon(SHARED.theme.getIcon("forward"))
|
||||
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
|
||||
@@ -728,7 +778,9 @@ class GuiDocViewHeader(QWidget):
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
|
||||
).format(colText.red(), colText.green(), colText.blue())
|
||||
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
|
||||
|
||||
self.outlineButton.setStyleSheet(buttonStyleMenu)
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.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.Text, SHARED.theme.colText)
|
||||
self.setPalette(palette)
|
||||
self.docTitle.setPalette(palette)
|
||||
self.itemTitle.setPalette(palette)
|
||||
return
|
||||
|
||||
def setTitleFromHandle(self, tHandle: str | None) -> None:
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Sets the document title from the handle, or alternatively,
|
||||
set the whole document path.
|
||||
"""
|
||||
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:
|
||||
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)]
|
||||
)))
|
||||
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.forwardButton.setVisible(True)
|
||||
self.outlineButton.setVisible(True)
|
||||
self.closeButton.setVisible(True)
|
||||
self.refreshButton.setVisible(True)
|
||||
|
||||
@@ -790,15 +836,14 @@ class GuiDocViewHeader(QWidget):
|
||||
@pyqtSlot()
|
||||
def _closeDocument(self) -> None:
|
||||
"""Trigger the close editor/viewer on the main window."""
|
||||
self.mainGui.closeDocViewer()
|
||||
self.clearHeader()
|
||||
self.docViewer.closeDocumentRequest.emit()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _refreshDocument(self) -> None:
|
||||
"""Reload the content of the document."""
|
||||
if self.docViewer.docHandle == self.mainGui.docEditor.docHandle:
|
||||
self.mainGui.saveDocument()
|
||||
self.docViewer.reloadText()
|
||||
self.docViewer.reloadDocumentRequest.emit()
|
||||
return
|
||||
|
||||
##
|
||||
@@ -829,7 +874,6 @@ class GuiDocViewFooter(QWidget):
|
||||
logger.debug("Create: GuiDocViewFooter")
|
||||
|
||||
self.docViewer = docViewer
|
||||
self.mainGui = docViewer.mainGui
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
|
||||
@@ -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,16 @@ class GuiTextDocument(QTextDocument):
|
||||
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
|
||||
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
|
||||
##
|
||||
|
||||
@@ -196,12 +196,12 @@ class GuiMainMenu(QMenuBar):
|
||||
# Document > Open
|
||||
self.aOpenDoc = self.docuMenu.addAction(self.tr("Open Document"))
|
||||
self.aOpenDoc.setShortcut("Ctrl+O")
|
||||
self.aOpenDoc.triggered.connect(lambda: self.mainGui.openSelectedItem())
|
||||
self.aOpenDoc.triggered.connect(self.mainGui.openSelectedItem)
|
||||
|
||||
# Document > Save
|
||||
self.aSaveDoc = self.docuMenu.addAction(self.tr("Save Document"))
|
||||
self.aSaveDoc.setShortcut("Ctrl+S")
|
||||
self.aSaveDoc.triggered.connect(lambda: self.mainGui.saveDocument())
|
||||
self.aSaveDoc.triggered.connect(self.mainGui.saveDocument)
|
||||
|
||||
# Document > Close
|
||||
self.aCloseDoc = self.docuMenu.addAction(self.tr("Close Document"))
|
||||
@@ -219,7 +219,7 @@ class GuiMainMenu(QMenuBar):
|
||||
# Document > Close Preview
|
||||
self.aCloseView = self.docuMenu.addAction(self.tr("Close Document View"))
|
||||
self.aCloseView.setShortcut("Ctrl+Shift+R")
|
||||
self.aCloseView.triggered.connect(lambda: self.mainGui.closeDocViewer())
|
||||
self.aCloseView.triggered.connect(self.mainGui.closeDocViewer)
|
||||
|
||||
# Document > Separator
|
||||
self.docuMenu.addSeparator()
|
||||
|
||||
@@ -142,7 +142,6 @@ class GuiProjectView(QWidget):
|
||||
self.requestDeleteItem = self.projTree.requestDeleteItem
|
||||
self.getSelectedHandle = self.projTree.getSelectedHandle
|
||||
self.changedSince = self.projTree.changedSince
|
||||
self.createNewNote = self.projTree.createNewNote
|
||||
|
||||
return
|
||||
|
||||
@@ -240,6 +239,12 @@ class GuiProjectView(QWidget):
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
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
|
||||
|
||||
|
||||
@@ -606,18 +611,18 @@ class GuiProjectTree(QTreeWidget):
|
||||
self._timeChanged = 0.0
|
||||
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
|
||||
editor to create note files for unknown tags.
|
||||
"""
|
||||
rHandle = SHARED.project.tree.findRoot(itemClass)
|
||||
if rHandle:
|
||||
tHandle = SHARED.project.newFile(tag, rHandle)
|
||||
if tHandle:
|
||||
if itemClass != nwItemClass.NO_CLASS:
|
||||
if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
|
||||
self.newTreeItem(nwItemType.ROOT, itemClass)
|
||||
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")
|
||||
self.revealNewTreeItem(tHandle, wordCount=True)
|
||||
return True
|
||||
return False
|
||||
return
|
||||
|
||||
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
||||
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
|
||||
|
||||
+103
-101
@@ -104,9 +104,6 @@ class GuiMain(QMainWindow):
|
||||
# Initialise UserData Instance
|
||||
SHARED.initSharedData(self, GuiTheme())
|
||||
|
||||
# Core Settings
|
||||
self.isFocusMode = False
|
||||
|
||||
# Prepare Main Window
|
||||
self.resize(*CONFIG.mainWinSize)
|
||||
self._updateWindowTitle()
|
||||
@@ -233,6 +230,7 @@ class GuiMain(QMainWindow):
|
||||
SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
|
||||
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
|
||||
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
|
||||
SHARED.focusModeChanged.connect(self._focusModeChanged)
|
||||
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
|
||||
SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
|
||||
SHARED.indexScannedText.connect(self.projView.updateItemValues)
|
||||
@@ -275,9 +273,12 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
|
||||
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
||||
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
|
||||
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
|
||||
|
||||
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
|
||||
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.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
||||
|
||||
@@ -401,7 +402,7 @@ class GuiMain(QMainWindow):
|
||||
if saveOK:
|
||||
self.closeDocument()
|
||||
self.docViewer.clearNavHistory()
|
||||
self.closeDocViewer(byUser=False)
|
||||
self.closeViewerPanel(byUser=False)
|
||||
|
||||
self.docViewerPanel.closeProjectTasks()
|
||||
self.outlineView.closeProjectTasks()
|
||||
@@ -523,24 +524,19 @@ class GuiMain(QMainWindow):
|
||||
# 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."""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
# Disable focus mode if it is active
|
||||
if self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
|
||||
self.docEditor.saveCursorPosition()
|
||||
if self.docEditor.docChanged:
|
||||
self.saveDocument()
|
||||
self.docEditor.clearEditor()
|
||||
if not beforeOpen:
|
||||
self.novelView.setActiveHandle(None)
|
||||
|
||||
return True
|
||||
if SHARED.hasProject:
|
||||
# Disable focus mode if it is active
|
||||
if SHARED.focusMode:
|
||||
SHARED.setFocusMode(False)
|
||||
self.docEditor.saveCursorPosition()
|
||||
if self.docEditor.docChanged:
|
||||
self.saveDocument()
|
||||
self.docEditor.clearEditor()
|
||||
if not beforeOpen:
|
||||
self.novelView.setActiveHandle(None)
|
||||
return
|
||||
|
||||
def openDocument(self, tHandle: str | None, tLine: int | None = None,
|
||||
changeFocus: bool = True, doScroll: bool = False) -> bool:
|
||||
@@ -604,13 +600,12 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return False
|
||||
|
||||
def saveDocument(self) -> bool:
|
||||
@pyqtSlot()
|
||||
def saveDocument(self) -> None:
|
||||
"""Save the current documents."""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
self.docEditor.saveText()
|
||||
return True
|
||||
if SHARED.hasProject:
|
||||
self.docEditor.saveText()
|
||||
return
|
||||
|
||||
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
|
||||
"""Load a document for viewing in the view panel."""
|
||||
@@ -640,7 +635,8 @@ class GuiMain(QMainWindow):
|
||||
self._changeView(nwView.EDITOR)
|
||||
|
||||
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():
|
||||
cursorVisible = self.docEditor.cursorIsVisible()
|
||||
bPos = self.splitMain.sizes()
|
||||
@@ -713,80 +709,71 @@ class GuiMain(QMainWindow):
|
||||
# Tree Item Actions
|
||||
##
|
||||
|
||||
def openSelectedItem(self) -> bool:
|
||||
@pyqtSlot()
|
||||
def openSelectedItem(self) -> None:
|
||||
"""Open the selected item from the tree that is currently
|
||||
active. It is not checked that the item is actually a document.
|
||||
That should be handled by the openDocument function.
|
||||
"""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
if SHARED.hasProject:
|
||||
tHandle = None
|
||||
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
|
||||
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 False
|
||||
if tHandle and sTitle:
|
||||
if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
tLine = hItem.line
|
||||
if tHandle:
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
|
||||
|
||||
if tHandle is not None and sTitle is not None:
|
||||
hItem = SHARED.project.index.getItemHeading(tHandle, sTitle)
|
||||
if hItem is not None:
|
||||
tLine = hItem.line
|
||||
return
|
||||
|
||||
if tHandle is not None:
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
|
||||
|
||||
return True
|
||||
|
||||
def editItemLabel(self, tHandle: str | None = None) -> bool:
|
||||
def editItemLabel(self, tHandle: str | None = None) -> None:
|
||||
"""Open the edit item dialog."""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode):
|
||||
tHandle = self.docEditor.docHandle
|
||||
self.projView.renameTreeItem(tHandle)
|
||||
return True
|
||||
if SHARED.hasProject:
|
||||
if tHandle is None and (self.docEditor.anyFocus() or SHARED.focusMode):
|
||||
tHandle = self.docEditor.docHandle
|
||||
self.projView.renameTreeItem(tHandle)
|
||||
return
|
||||
|
||||
def rebuildTrees(self) -> None:
|
||||
"""Rebuild the project tree."""
|
||||
self.projView.populateTree()
|
||||
return
|
||||
|
||||
def rebuildIndex(self, beQuiet: bool = False) -> bool:
|
||||
def rebuildIndex(self, beQuiet: bool = False) -> None:
|
||||
"""Rebuild the entire index."""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
if SHARED.hasProject:
|
||||
logger.info("Rebuilding index ...")
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
tStart = time()
|
||||
|
||||
logger.info("Rebuilding index ...")
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
tStart = time()
|
||||
self.projView.saveProjectTasks()
|
||||
SHARED.project.index.rebuildIndex()
|
||||
self.projView.populateTree()
|
||||
self.novelView.refreshTree()
|
||||
|
||||
self.projView.saveProjectTasks()
|
||||
SHARED.project.index.rebuildIndex()
|
||||
self.projView.populateTree()
|
||||
self.novelView.refreshTree()
|
||||
tEnd = time()
|
||||
self.mainStatus.setStatusMessage(
|
||||
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
|
||||
)
|
||||
self.docEditor.updateTagHighLighting()
|
||||
self._updateStatusWordCount()
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
tEnd = time()
|
||||
self.mainStatus.setStatusMessage(
|
||||
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
|
||||
)
|
||||
self.docEditor.updateTagHighLighting()
|
||||
self._updateStatusWordCount()
|
||||
qApp.restoreOverrideCursor()
|
||||
if not beQuiet:
|
||||
SHARED.info(self.tr("The project index has been successfully rebuilt."))
|
||||
|
||||
if not beQuiet:
|
||||
SHARED.info(self.tr("The project index has been successfully rebuilt."))
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
##
|
||||
# Main Dialogs
|
||||
@@ -896,15 +883,14 @@ class GuiMain(QMainWindow):
|
||||
SHARED.error(self.tr("Could not initialise the dialog."))
|
||||
return
|
||||
|
||||
def reportConfErr(self) -> bool:
|
||||
def reportConfErr(self) -> None:
|
||||
"""Checks if the Config module has any errors to report, and let
|
||||
the user know if this is the case. The Config module caches
|
||||
errors since it is initialised before the GUI itself.
|
||||
"""
|
||||
if CONFIG.hasError:
|
||||
SHARED.error(CONFIG.errorText())
|
||||
return True
|
||||
return False
|
||||
return
|
||||
|
||||
##
|
||||
# Main Window Actions
|
||||
@@ -922,7 +908,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.info("Exiting novelWriter")
|
||||
|
||||
if not self.isFocusMode:
|
||||
if not SHARED.focusMode:
|
||||
CONFIG.setMainPanePos(self.splitMain.sizes())
|
||||
CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
|
||||
if self.docViewerPanel.isVisible():
|
||||
@@ -943,7 +929,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def closeDocViewer(self, byUser: bool = True) -> bool:
|
||||
def closeViewerPanel(self, byUser: bool = True) -> bool:
|
||||
"""Close the document view panel."""
|
||||
self.docViewer.clearViewer()
|
||||
if byUser:
|
||||
@@ -991,32 +977,40 @@ class GuiMain(QMainWindow):
|
||||
SHARED.project.data.setLastHandle(None, "editor")
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def closeDocViewer(self) -> None:
|
||||
"""Close the document viewer."""
|
||||
self.closeViewerPanel()
|
||||
SHARED.project.data.setLastHandle(None, "viewer")
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
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.
|
||||
"""
|
||||
if self.docEditor.docHandle is None:
|
||||
logger.error("No document open, so not activating Focus Mode")
|
||||
return
|
||||
|
||||
self.isFocusMode = not self.isFocusMode
|
||||
if self.isFocusMode:
|
||||
if focusMode:
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.switchFocus(nwWidget.EDITOR)
|
||||
else:
|
||||
logger.debug("Deactivating Focus Mode")
|
||||
|
||||
cursorVisible = self.docEditor.cursorIsVisible()
|
||||
isVisible = not self.isFocusMode
|
||||
isVisible = not focusMode
|
||||
self.treePane.setVisible(isVisible)
|
||||
self.mainStatus.setVisible(isVisible)
|
||||
self.mainMenu.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.docHeader.updateFocusMode()
|
||||
|
||||
if self.splitView.isVisible():
|
||||
self.splitView.setVisible(False)
|
||||
@@ -1025,7 +1019,6 @@ class GuiMain(QMainWindow):
|
||||
|
||||
if cursorVisible:
|
||||
self.docEditor.ensureCursorVisibleNoCentre()
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(nwWidget)
|
||||
@@ -1154,6 +1147,15 @@ class GuiMain(QMainWindow):
|
||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||
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)
|
||||
def _changeView(self, view: nwView) -> None:
|
||||
"""Handle the requested change of view from the GuiViewBar."""
|
||||
@@ -1266,8 +1268,8 @@ class GuiMain(QMainWindow):
|
||||
"""Process escape keypress in the main window."""
|
||||
if self.docEditor.docSearch.isVisible():
|
||||
self.docEditor.closeSearch()
|
||||
elif self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
elif SHARED.focusMode:
|
||||
SHARED.setFocusMode(False)
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
|
||||
@@ -57,6 +57,7 @@ class SharedData(QObject):
|
||||
projectStatusChanged = pyqtSignal(bool)
|
||||
projectStatusMessage = pyqtSignal(str)
|
||||
spellLanguageChanged = pyqtSignal(str, str)
|
||||
focusModeChanged = pyqtSignal(bool)
|
||||
indexScannedText = pyqtSignal(str)
|
||||
indexChangedTags = pyqtSignal(list, list)
|
||||
indexCleared = pyqtSignal()
|
||||
@@ -76,6 +77,7 @@ class SharedData(QObject):
|
||||
self._lastAlert = ""
|
||||
self._idleTime = 0.0
|
||||
self._idleRefTime = time()
|
||||
self._focusMode = False
|
||||
|
||||
return
|
||||
|
||||
@@ -111,6 +113,11 @@ class SharedData(QObject):
|
||||
raise Exception("SharedData class not fully initialised")
|
||||
return self._spelling
|
||||
|
||||
@property
|
||||
def focusMode(self) -> bool:
|
||||
"""Return the Focus Mode state."""
|
||||
return self._focusMode
|
||||
|
||||
@property
|
||||
def hasProject(self) -> bool:
|
||||
"""Return True if the project instance is populated."""
|
||||
@@ -131,6 +138,17 @@ class SharedData(QObject):
|
||||
"""Return the last alert message."""
|
||||
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
|
||||
##
|
||||
@@ -323,6 +341,7 @@ class SharedData(QObject):
|
||||
self._project = NWProject()
|
||||
self._spelling = NWSpellEnchant(self._project)
|
||||
self.updateSpellCheckLanguage()
|
||||
self._focusMode = False
|
||||
return
|
||||
|
||||
def _resetIdleTimer(self) -> None:
|
||||
|
||||
@@ -705,6 +705,16 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
||||
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
|
||||
# ============
|
||||
assert index.getSingleTag("jane") == (
|
||||
|
||||
@@ -30,7 +30,9 @@ from PyQt5.QtCore import QThreadPool, Qt
|
||||
from PyQt5.QtWidgets import QAction, QMenu, qApp
|
||||
|
||||
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.gui.doceditor import GuiDocEditor, GuiDocToolBar
|
||||
from novelwriter.text.counting import standardCounter
|
||||
@@ -47,7 +49,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert nwGUI.openDocument(C.hSceneDoc)
|
||||
|
||||
nwGUI.docEditor.setPlainText("### Lorem Ipsum\n\n%s" % ipsumText[0])
|
||||
assert nwGUI.saveDocument()
|
||||
nwGUI.saveDocument()
|
||||
|
||||
# Check Defaults
|
||||
qDoc = nwGUI.docEditor.document()
|
||||
@@ -58,6 +60,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert nwGUI.docEditor.docHeader.itemTitle.text() == (
|
||||
"Novel \u203a New Chapter \u203a New Scene"
|
||||
)
|
||||
assert nwGUI.docEditor.docHeader._docOutline == {0: "### New Scene"}
|
||||
|
||||
# Check that editor handles settings
|
||||
CONFIG.textFont = ""
|
||||
@@ -84,6 +87,11 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
# Header
|
||||
# ======
|
||||
|
||||
# Go to outline
|
||||
nwGUI.docEditor.setCursorLine(3)
|
||||
nwGUI.docEditor.docHeader.outlineMenu.actions()[0].trigger()
|
||||
assert nwGUI.docEditor.getCursorPosition() == 0
|
||||
|
||||
# Select item from header
|
||||
with qtbot.waitSignal(nwGUI.docEditor.requestProjectItemSelected, timeout=1000) as signal:
|
||||
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)
|
||||
nwGUI.docEditor.replaceText(longText)
|
||||
assert nwGUI.saveDocument() is True
|
||||
assert nwGUI.closeDocument() is True
|
||||
nwGUI.saveDocument()
|
||||
nwGUI.closeDocument()
|
||||
|
||||
# Invalid handle
|
||||
assert nwGUI.docEditor.loadText("abcdefghijklm") is False
|
||||
@@ -132,7 +140,7 @@ def testGuiEditor_LoadText(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
|
||||
# Load empty document
|
||||
nwGUI.docEditor.replaceText("")
|
||||
assert nwGUI.saveDocument() is True
|
||||
nwGUI.saveDocument()
|
||||
assert nwGUI.docEditor.loadText(C.hSceneDoc) is True
|
||||
assert nwGUI.docEditor.toPlainText() == ""
|
||||
|
||||
@@ -1478,7 +1486,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
cHandle = SHARED.project.newFile("Jane Doe", C.hCharRoot)
|
||||
assert nwGUI.openDocument(cHandle) is True
|
||||
nwGUI.docEditor.replaceText(text)
|
||||
assert nwGUI.saveDocument() is True
|
||||
nwGUI.saveDocument()
|
||||
assert nwGUI.projView.projTree.revealNewTreeItem(cHandle)
|
||||
nwGUI.docEditor.updateTagHighLighting()
|
||||
|
||||
@@ -1508,7 +1516,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert nwGUI.docViewer._docHandle is None
|
||||
assert nwGUI.docEditor._processTag(follow=True) is nwTrinary.POSITIVE
|
||||
assert nwGUI.docViewer._docHandle == cHandle
|
||||
assert nwGUI.closeDocViewer() is True
|
||||
assert nwGUI.closeViewerPanel() is True
|
||||
assert nwGUI.docViewer._docHandle is None
|
||||
|
||||
# On Unknown Tag, Create It
|
||||
@@ -1521,7 +1529,12 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||
assert "0000000000012" not in SHARED.project.tree
|
||||
nwGUI.docEditor.setCursorPosition(42)
|
||||
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)
|
||||
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)
|
||||
assert nwGUI.openDocument(cHandle) is True
|
||||
nwGUI.docEditor.replaceText(text)
|
||||
assert nwGUI.saveDocument() is True
|
||||
nwGUI.saveDocument()
|
||||
assert nwGUI.projView.projTree.revealNewTreeItem(cHandle)
|
||||
|
||||
docEditor = nwGUI.docEditor
|
||||
@@ -1686,13 +1699,13 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
|
||||
|
||||
threadPool = MockThreadPool()
|
||||
monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool)
|
||||
nwGUI.docEditor.wcTimerDoc.blockSignals(True)
|
||||
nwGUI.docEditor.wcTimerSel.blockSignals(True)
|
||||
nwGUI.docEditor.timerDoc.blockSignals(True)
|
||||
nwGUI.docEditor.timerSel.blockSignals(True)
|
||||
|
||||
buildTestProject(nwGUI, projPath)
|
||||
|
||||
# Run on an empty document
|
||||
nwGUI.docEditor._runDocCounter()
|
||||
nwGUI.docEditor._runDocumentTasks()
|
||||
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
||||
nwGUI.docEditor._updateDocCounts(0, 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
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr(nwGUI.docEditor.wCounterDoc, "isRunning", lambda *a: True)
|
||||
nwGUI.docEditor._runDocCounter()
|
||||
nwGUI.docEditor._runDocumentTasks()
|
||||
assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
|
||||
|
||||
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)"
|
||||
|
||||
# Run the full word counter
|
||||
nwGUI.docEditor._runDocCounter()
|
||||
nwGUI.docEditor._runDocumentTasks()
|
||||
assert threadPool.objectID() == id(nwGUI.docEditor.wCounterDoc)
|
||||
|
||||
nwGUI.docEditor.wCounterDoc.run()
|
||||
|
||||
@@ -185,12 +185,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
||||
nwItem.setName("Test Title") # type: ignore
|
||||
assert nwItem.itemName == "Test Title" # type: ignore
|
||||
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
|
||||
CONFIG.showFullPath = False
|
||||
docViewer.updateDocInfo("4c4f28287af27")
|
||||
assert docViewer.docHeader.docTitle.text() == "Test Title"
|
||||
assert docViewer.docHeader.itemTitle.text() == "Test Title"
|
||||
CONFIG.showFullPath = True
|
||||
|
||||
# Document footer show/hide synopsis
|
||||
|
||||
@@ -49,15 +49,10 @@ def testGuiMain_ProjectBlocker(nwGUI):
|
||||
# Test no-project blocking
|
||||
assert nwGUI.closeProject() is True
|
||||
assert nwGUI.saveProject() is False
|
||||
assert nwGUI.closeDocument() is False
|
||||
assert nwGUI.openDocument(None) is False
|
||||
assert nwGUI.openNextDocument(None) is False
|
||||
assert nwGUI.saveDocument() is False
|
||||
assert nwGUI.viewDocument(None) 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
|
||||
|
||||
@@ -109,7 +104,8 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
buildTestProject(nwGUI, projPath)
|
||||
|
||||
sHandle = "000000000000f"
|
||||
assert nwGUI.openSelectedItem() is False
|
||||
nwGUI.openSelectedItem()
|
||||
assert nwGUI.docEditor.docHandle is None
|
||||
|
||||
# Project Tree has focus
|
||||
nwGUI._changeView(nwView.PROJECT)
|
||||
@@ -121,7 +117,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
nwGUI.closeDocument()
|
||||
|
||||
# Novel Tree has focus
|
||||
nwGUI._changeView(nwView.NOVEL)
|
||||
@@ -133,7 +129,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
nwGUI.novelView.novelTree.setCurrentItem(selItem)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
nwGUI.closeDocument()
|
||||
|
||||
# Project Outline has focus
|
||||
nwGUI._changeView(nwView.OUTLINE)
|
||||
@@ -145,7 +141,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
nwGUI.closeDocument()
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
@@ -238,7 +234,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
nwGUI.openSelectedItem()
|
||||
|
||||
# Text Editor
|
||||
# ===========
|
||||
@@ -265,7 +261,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||
@@ -287,7 +283,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
nwGUI.projView.projTree.clearSelection()
|
||||
nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True)
|
||||
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
nwGUI.openSelectedItem()
|
||||
|
||||
# Add Some Text
|
||||
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.hChapterDir).setExpanded(True)
|
||||
nwGUI.projView.projTree._getTreeItem(C.hSceneDoc).setSelected(True)
|
||||
assert nwGUI.openSelectedItem()
|
||||
nwGUI.openSelectedItem()
|
||||
|
||||
# Type something into the document
|
||||
nwGUI.switchFocus(nwWidget.EDITOR)
|
||||
@@ -524,8 +520,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
|
||||
# Save the document
|
||||
assert docEditor.docChanged
|
||||
assert nwGUI.saveDocument()
|
||||
assert not docEditor.docChanged
|
||||
nwGUI.saveDocument()
|
||||
assert docEditor.docChanged is False
|
||||
nwGUI.rebuildIndex()
|
||||
|
||||
# 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.viewDocument(C.hSceneDoc)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeDocViewer()
|
||||
assert nwGUI.closeViewerPanel()
|
||||
|
||||
# Check the files
|
||||
projFile = projPath / "nwProject.nwx"
|
||||
@@ -575,7 +571,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
|
||||
"""Test various features of the main window."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
assert nwGUI.isFocusMode is False
|
||||
assert SHARED.focusMode is False
|
||||
|
||||
# Focus Mode
|
||||
# ==========
|
||||
|
||||
Reference in New Issue
Block a user