Add table of contents code to novel details panel

This commit is contained in:
Veronica Berglyd Olsen
2024-01-25 21:25:43 +01:00
parent 6622a7e8e0
commit 23713bb791
4 changed files with 326 additions and 26 deletions
+12 -9
View File
@@ -528,17 +528,20 @@ class NWIndex:
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return return
def getNovelWordCount(self, activeOnly: bool = True) -> int: def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
"""Count the number of words in the novel project.""" """Count the number of words in one or all novel roots."""
wCount = 0 return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): rHandle=rootHandle, activeOnly=activeOnly
wCount += hItem.wordCount ))
return wCount
def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]: def getNovelTitleCounts(
"""Count the number of titles in the novel project.""" self, rootHandle: str | None = None, activeOnly: bool = True
) -> list[int]:
"""Count the number of titles in one or all novel roots."""
hCount = [0, 0, 0, 0, 0] hCount = [0, 0, 0, 0, 0]
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): for _, _, hItem in self._itemIndex.iterNovelStructure(
rHandle=rootHandle, activeOnly=activeOnly
):
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
hCount[iLevel] += 1 hCount[iLevel] += 1
return hCount return hCount
+3 -1
View File
@@ -73,7 +73,9 @@ VALID_MAP: dict[str, set[str]] = {
"colWidths", "hideInactive", "colWidths", "hideInactive",
}, },
"GuiNovelDetails": { "GuiNovelDetails": {
"winWidth", "winHeight", "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
"novelRoot",
}, },
} }
+306 -16
View File
@@ -23,23 +23,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import math
import logging import logging
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QCloseEvent from PyQt5.QtGui import QCloseEvent
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout, QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout,
QWidget QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HEADER_SIZE = 1.4
class GuiNovelDetails(QDialog): class GuiNovelDetails(QDialog):
@@ -72,6 +78,7 @@ class GuiNovelDetails(QDialog):
# Novel Selector # Novel Selector
self.novelSelector = NovelSelector(self) self.novelSelector = NovelSelector(self)
self.novelSelector.refreshNovelList() self.novelSelector.refreshNovelList()
self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", ""))
# SideBar # SideBar
self.sidebar = NPagedSideBar(self) self.sidebar = NPagedSideBar(self)
@@ -83,9 +90,11 @@ class GuiNovelDetails(QDialog):
# Content # Content
self.overviewPage = _OverviewPage(self) self.overviewPage = _OverviewPage(self)
self.contentsPage = _ContentsPage(self)
self.mainStack = QStackedWidget(self) self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.overviewPage) self.mainStack.addWidget(self.overviewPage)
self.mainStack.addWidget(self.contentsPage)
# Buttons # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
@@ -111,6 +120,10 @@ class GuiNovelDetails(QDialog):
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.setSizeGripEnabled(True) self.setSizeGripEnabled(True)
# Connect Signals
self.novelSelector.novelSelectionChanged.connect(self.overviewPage.novelValueChanged)
self.novelSelector.novelSelectionChanged.connect(self.contentsPage.novelValueChanged)
logger.debug("Ready: GuiNovelDetails") logger.debug("Ready: GuiNovelDetails")
return return
@@ -126,6 +139,8 @@ class GuiNovelDetails(QDialog):
def updateValues(self) -> None: def updateValues(self) -> None:
"""Load the dialogs initial values.""" """Load the dialogs initial values."""
self.overviewPage.updateProjectData() self.overviewPage.updateProjectData()
self.overviewPage.novelValueChanged(self.novelSelector.handle)
self.contentsPage.novelValueChanged(self.novelSelector.handle)
return return
## ##
@@ -148,6 +163,8 @@ class GuiNovelDetails(QDialog):
"""Process a user request to switch page.""" """Process a user request to switch page."""
if pageId == self.PAGE_OVERVIEW: if pageId == self.PAGE_OVERVIEW:
self.mainStack.setCurrentWidget(self.overviewPage) self.mainStack.setCurrentWidget(self.overviewPage)
elif pageId == self.PAGE_CONTENTS:
self.mainStack.setCurrentWidget(self.contentsPage)
return return
## ##
@@ -158,11 +175,16 @@ class GuiNovelDetails(QDialog):
"""Save the user GUI settings.""" """Save the user GUI settings."""
winWidth = CONFIG.rpxInt(self.width()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
novelRoot = self.novelSelector.handle
logger.debug("Saving State: GuiNovelDetails") logger.debug("Saving State: GuiNovelDetails")
options = SHARED.project.options options = SHARED.project.options
options.setValue("GuiNovelDetails", "winWidth", winWidth) options.setValue("GuiNovelDetails", "winWidth", winWidth)
options.setValue("GuiNovelDetails", "winHeight", winHeight) options.setValue("GuiNovelDetails", "winHeight", winHeight)
options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
self.contentsPage.saveSettings()
return return
# END Class GuiNovelDetails # END Class GuiNovelDetails
@@ -179,24 +201,32 @@ class _OverviewPage(QWidget):
vPx = CONFIG.pxInt(4) vPx = CONFIG.pxInt(4)
# Project Info # Project Info
self.projLabel = NColourLabel(self.tr("Project"), SHARED.theme.helpText, self, 1.5) self.projLabel = NColourLabel(
self.tr("Project"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE
)
self.projName = QLabel("", self) self.projName = QLabel("", self)
self.projWords = QLabel("", self) self.projWords = QLabel("", self)
self.projNovels = QLabel("", self)
self.projNotes = QLabel("", self)
self.projRevisions = QLabel("", self) self.projRevisions = QLabel("", self)
self.projEditTime = QLabel("", self) self.projEditTime = QLabel("", self)
self.projForm = QFormLayout() self.projForm = QFormLayout()
self.projForm.addRow("<b>%s</b>" % self.tr("Name"), self.projName) self.projForm.addRow("<b>{0}</b>".format(self.tr("Name")), self.projName)
self.projForm.addRow("<b>%s</b>" % self.tr("Word Count"), self.projWords) self.projForm.addRow("<b>{0}</b>".format(self.tr("Revisions")), self.projRevisions)
self.projForm.addRow("<b>%s</b>" % self.tr("Revisions"), self.projRevisions) self.projForm.addRow("<b>{0}</b>".format(self.tr("Editing Time")), self.projEditTime)
self.projForm.addRow("<b>%s</b>" % self.tr("Editing Time"), self.projEditTime) self.projForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.projWords)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Novels")), self.projNovels)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Notes ")), self.projNotes)
self.projForm.setContentsMargins(mPx, 0, 0, 0) self.projForm.setContentsMargins(mPx, 0, 0, 0)
self.projForm.setHorizontalSpacing(hPx) self.projForm.setHorizontalSpacing(hPx)
self.projForm.setVerticalSpacing(vPx) self.projForm.setVerticalSpacing(vPx)
# Novel Info # Novel Info
self.novelLabel = NColourLabel(self.tr("Novel"), SHARED.theme.helpText, self, 1.5) self.novelLabel = NColourLabel(
self.tr("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE
)
self.novelName = QLabel("", self) self.novelName = QLabel("", self)
self.novelWords = QLabel("", self) self.novelWords = QLabel("", self)
@@ -204,10 +234,10 @@ class _OverviewPage(QWidget):
self.novelScenes = QLabel("", self) self.novelScenes = QLabel("", self)
self.novelForm = QFormLayout() self.novelForm = QFormLayout()
self.novelForm.addRow("<b>%s</b>" % self.tr("Name"), self.novelName) self.novelForm.addRow("<b>{0}</b>".format(self.tr("Name")), self.novelName)
self.novelForm.addRow("<b>%s</b>" % self.tr("Word Count"), self.novelWords) self.novelForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.novelWords)
self.novelForm.addRow("<b>%s</b>" % self.tr("Chapters"), self.novelChapters) self.novelForm.addRow("<b>{0}</b>".format(self.tr("Chapters")), self.novelChapters)
self.novelForm.addRow("<b>%s</b>" % self.tr("Scenes"), self.novelScenes) self.novelForm.addRow("<b>{0}</b>".format(self.tr("Scenes")), self.novelScenes)
self.novelForm.setContentsMargins(mPx, 0, 0, 0) self.novelForm.setContentsMargins(mPx, 0, 0, 0)
self.novelForm.setHorizontalSpacing(hPx) self.novelForm.setHorizontalSpacing(hPx)
self.novelForm.setVerticalSpacing(vPx) self.novelForm.setVerticalSpacing(vPx)
@@ -233,10 +263,270 @@ class _OverviewPage(QWidget):
def updateProjectData(self) -> None: def updateProjectData(self) -> None:
"""Load information about the project.""" """Load information about the project."""
project = SHARED.project project = SHARED.project
project.updateWordCounts()
wcNovel, wcNotes = project.data.currCounts
self.projName.setText(project.data.name) self.projName.setText(project.data.name)
self.projWords.setText(f"{project.index.getNovelWordCount():n}")
self.projRevisions.setText(f"{project.data.saveCount:n}") self.projRevisions.setText(f"{project.data.saveCount:n}")
self.projEditTime.setText(formatTime(project.currentEditTime)) self.projEditTime.setText(formatTime(project.currentEditTime))
self.projWords.setText(f"{wcNovel + wcNotes:n}")
self.projNovels.setText(f"{wcNovel:n}")
self.projNotes.setText(f"{wcNotes:n}")
return
##
# Public Slots
##
@pyqtSlot(str)
def novelValueChanged(self, tHandle: str) -> None:
"""Refresh the data for the selected novel."""
project = SHARED.project
if nwItem := project.tree[tHandle]:
self.novelName.setText(nwItem.itemName)
nwCount = project.index.getNovelWordCount(rootHandle=tHandle)
self.novelWords.setText(f"{nwCount:n}")
hCounts = project.index.getNovelTitleCounts(rootHandle=tHandle)
self.novelChapters.setText(f"{hCounts[2]:n}")
self.novelScenes.setText(f"{hCounts[3]:n}")
return return
# END Class _OverviewPage # END Class _OverviewPage
class _ContentsPage(QWidget):
C_TITLE = 0
C_WORDS = 1
C_PAGES = 2
C_PAGE = 3
C_PROG = 4
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._data = []
self._currentRoot = None
iPx = SHARED.theme.baseIconSize
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
options = SHARED.project.options
# Title
self.contentLabel = NColourLabel(
self.tr("Table of Contents"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE
)
# Contents Tree
self.tocTree = QTreeWidget(self)
self.tocTree.setIconSize(QSize(iPx, iPx))
self.tocTree.setIndentation(0)
self.tocTree.setColumnCount(6)
self.tocTree.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.tocTree.setHeaderLabels([
self.tr("Title"),
self.tr("Words"),
self.tr("Pages"),
self.tr("Page"),
self.tr("Progress"),
"",
])
treeHeadItem = self.tocTree.headerItem()
if treeHeadItem:
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignmentFlag.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignmentFlag.AlignRight)
treeHeader = self.tocTree.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx)
wCol0 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol0", 200))
wCol1 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol1", 60))
wCol2 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol2", 60))
wCol3 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol3", 60))
wCol4 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol4", 90))
self.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1)
self.tocTree.setColumnWidth(2, wCol2)
self.tocTree.setColumnWidth(3, wCol3)
self.tocTree.setColumnWidth(4, wCol4)
self.tocTree.setColumnWidth(5, hPx)
# Options
wordsPerPage = options.getInt("GuiNovelDetails", "wordsPerPage", 350)
countFrom = options.getInt("GuiNovelDetails", "countFrom", 1)
clearDouble = options.getBool("GuiNovelDetails", "clearDouble", True)
self.wpLabel = QLabel(self.tr("Words per page"))
self.wpValue = QSpinBox(self)
self.wpValue.setMinimum(10)
self.wpValue.setMaximum(1000)
self.wpValue.setSingleStep(10)
self.wpValue.setValue(wordsPerPage)
self.wpValue.valueChanged.connect(self._populateTree)
self.poLabel = QLabel(self.tr("First page offset"))
self.poValue = QSpinBox(self)
self.poValue.setMinimum(1)
self.poValue.setMaximum(9999)
self.poValue.setSingleStep(1)
self.poValue.setValue(countFrom)
self.poValue.valueChanged.connect(self._populateTree)
self.dblLabel = QLabel(self.tr("Chapters on odd pages"))
self.dblValue = NSwitch(self, 2*iPx, iPx)
self.dblValue.setChecked(clearDouble)
self.dblValue.clicked.connect(self._populateTree)
self.optionsBox = QGridLayout()
self.optionsBox.addWidget(self.wpLabel, 0, 0)
self.optionsBox.addWidget(self.wpValue, 0, 1)
self.optionsBox.addWidget(self.dblLabel, 0, 3)
self.optionsBox.addWidget(self.dblValue, 0, 4)
self.optionsBox.addWidget(self.poLabel, 1, 0)
self.optionsBox.addWidget(self.poValue, 1, 1)
self.optionsBox.setHorizontalSpacing(hPx)
self.optionsBox.setVerticalSpacing(vPx)
self.optionsBox.setColumnStretch(2, 1)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.contentLabel)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
return
def saveSettings(self) -> None:
"""Save the user GUI settings."""
options = SHARED.project.options
options.setValue("GuiNovelDetails", "widthCol0", self.tocTree.columnWidth(0))
options.setValue("GuiNovelDetails", "widthCol1", self.tocTree.columnWidth(1))
options.setValue("GuiNovelDetails", "widthCol2", self.tocTree.columnWidth(2))
options.setValue("GuiNovelDetails", "widthCol3", self.tocTree.columnWidth(3))
options.setValue("GuiNovelDetails", "widthCol4", self.tocTree.columnWidth(4))
options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
return
def getColumnSizes(self) -> list[int]:
"""Return the column widths for the tree columns."""
retVals = [
self.tocTree.columnWidth(0),
self.tocTree.columnWidth(1),
self.tocTree.columnWidth(2),
self.tocTree.columnWidth(3),
self.tocTree.columnWidth(4),
]
return retVals
##
# Public Slots
##
@pyqtSlot(str)
def novelValueChanged(self, tHandle: str) -> None:
"""Refresh the tree with another root item."""
if tHandle != self._currentRoot:
self._prepareData(tHandle)
self._populateTree()
self._currentRoot = tHandle
return
##
# Private Slots
##
@pyqtSlot()
def _populateTree(self) -> None:
"""Set the content of the chapter/page tree."""
dblPages = self.dblValue.isChecked()
wpPage = self.wpValue.value()
fstPage = self.poValue.value() - 1
pTotal = 0
tPages = 1
theList = []
for _, tLevel, tTitle, wCount in self._data:
pCount = math.ceil(wCount/wpPage)
if dblPages:
pCount += pCount%2
pTotal += pCount
theList.append((tLevel, tTitle, wCount, pCount))
pMax = pTotal - fstPage
self.tocTree.clear()
for tLevel, tTitle, wCount, pCount in theList:
newItem = QTreeWidgetItem()
if tPages <= fstPage:
progPage = numberToRoman(tPages, True)
progText = ""
else:
cPage = tPages - fstPage
pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = SHARED.theme.getHeaderDecoration(tLevel)
if tTitle.strip() == "":
tTitle = self.tr("Untitled")
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}")
newItem.setText(self.C_PAGE, progPage)
newItem.setText(self.C_PROG, progText)
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
# Make pages and titles/partitions stand out
if tLevel < 2:
bFont = newItem.font(self.C_TITLE)
if tLevel == 0:
bFont.setItalic(True)
else:
bFont.setBold(True)
bFont.setUnderline(True)
newItem.setFont(self.C_TITLE, bFont)
tPages += pCount
self.tocTree.addTopLevelItem(newItem)
return
##
# Internal Functions
##
def _prepareData(self, rootHandle: str | None) -> None:
"""Extract the information from the project index."""
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._data = SHARED.project.index.getTableOfContents(rootHandle, 2)
self._data.append(("", 0, self.tr("END"), 0))
return
# END Class _ContentsPage
+5
View File
@@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"This is a story about Jane Smith.\n\n" "This is a story about Jane Smith.\n\n"
"Well, not really. She's still awesome though.\n" "Well, not really. She's still awesome though.\n"
)) ))
# Whole document # Whole document
cC, wC, pC = index.getCounts(nHandle) cC, wC, pC = index.getCounts(nHandle)
assert cC == 152 assert cC == 152
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Extract stats # Extract stats
assert index.getNovelWordCount(activeOnly=False) == 43 assert index.getNovelWordCount(activeOnly=False) == 43
assert index.getNovelWordCount(activeOnly=True) == 15 assert index.getNovelWordCount(activeOnly=True) == 15
assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=False) == 43
assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=True) == 15
assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=False) == 0
assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=True) == 0
assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0] assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0]
assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0] assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0]