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
return
def getNovelWordCount(self, activeOnly: bool = True) -> int:
"""Count the number of words in the novel project."""
wCount = 0
for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly):
wCount += hItem.wordCount
return wCount
def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
"""Count the number of words in one or all novel roots."""
return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
rHandle=rootHandle, activeOnly=activeOnly
))
def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]:
"""Count the number of titles in the novel project."""
def getNovelTitleCounts(
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]
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)
hCount[iLevel] += 1
return hCount
+3 -1
View File
@@ -73,7 +73,9 @@ VALID_MAP: dict[str, set[str]] = {
"colWidths", "hideInactive",
},
"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
import math
import logging
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout,
QWidget
QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget
)
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.novelselector import NovelSelector
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__)
HEADER_SIZE = 1.4
class GuiNovelDetails(QDialog):
@@ -72,6 +78,7 @@ class GuiNovelDetails(QDialog):
# Novel Selector
self.novelSelector = NovelSelector(self)
self.novelSelector.refreshNovelList()
self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", ""))
# SideBar
self.sidebar = NPagedSideBar(self)
@@ -83,9 +90,11 @@ class GuiNovelDetails(QDialog):
# Content
self.overviewPage = _OverviewPage(self)
self.contentsPage = _ContentsPage(self)
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.overviewPage)
self.mainStack.addWidget(self.contentsPage)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
@@ -111,6 +120,10 @@ class GuiNovelDetails(QDialog):
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
# Connect Signals
self.novelSelector.novelSelectionChanged.connect(self.overviewPage.novelValueChanged)
self.novelSelector.novelSelectionChanged.connect(self.contentsPage.novelValueChanged)
logger.debug("Ready: GuiNovelDetails")
return
@@ -126,6 +139,8 @@ class GuiNovelDetails(QDialog):
def updateValues(self) -> None:
"""Load the dialogs initial values."""
self.overviewPage.updateProjectData()
self.overviewPage.novelValueChanged(self.novelSelector.handle)
self.contentsPage.novelValueChanged(self.novelSelector.handle)
return
##
@@ -148,6 +163,8 @@ class GuiNovelDetails(QDialog):
"""Process a user request to switch page."""
if pageId == self.PAGE_OVERVIEW:
self.mainStack.setCurrentWidget(self.overviewPage)
elif pageId == self.PAGE_CONTENTS:
self.mainStack.setCurrentWidget(self.contentsPage)
return
##
@@ -158,11 +175,16 @@ class GuiNovelDetails(QDialog):
"""Save the user GUI settings."""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
novelRoot = self.novelSelector.handle
logger.debug("Saving State: GuiNovelDetails")
options = SHARED.project.options
options.setValue("GuiNovelDetails", "winWidth", winWidth)
options.setValue("GuiNovelDetails", "winHeight", winHeight)
options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
self.contentsPage.saveSettings()
return
# END Class GuiNovelDetails
@@ -179,24 +201,32 @@ class _OverviewPage(QWidget):
vPx = CONFIG.pxInt(4)
# 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.projWords = QLabel("", self)
self.projNovels = QLabel("", self)
self.projNotes = QLabel("", self)
self.projRevisions = QLabel("", self)
self.projEditTime = QLabel("", self)
self.projForm = QFormLayout()
self.projForm.addRow("<b>%s</b>" % self.tr("Name"), self.projName)
self.projForm.addRow("<b>%s</b>" % self.tr("Word Count"), self.projWords)
self.projForm.addRow("<b>%s</b>" % self.tr("Revisions"), self.projRevisions)
self.projForm.addRow("<b>%s</b>" % self.tr("Editing Time"), self.projEditTime)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Name")), self.projName)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Revisions")), self.projRevisions)
self.projForm.addRow("<b>{0}</b>".format(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.setHorizontalSpacing(hPx)
self.projForm.setVerticalSpacing(vPx)
# 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.novelWords = QLabel("", self)
@@ -204,10 +234,10 @@ class _OverviewPage(QWidget):
self.novelScenes = QLabel("", self)
self.novelForm = QFormLayout()
self.novelForm.addRow("<b>%s</b>" % self.tr("Name"), self.novelName)
self.novelForm.addRow("<b>%s</b>" % self.tr("Word Count"), self.novelWords)
self.novelForm.addRow("<b>%s</b>" % 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("Name")), self.novelName)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.novelWords)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Chapters")), self.novelChapters)
self.novelForm.addRow("<b>{0}</b>".format(self.tr("Scenes")), self.novelScenes)
self.novelForm.setContentsMargins(mPx, 0, 0, 0)
self.novelForm.setHorizontalSpacing(hPx)
self.novelForm.setVerticalSpacing(vPx)
@@ -233,10 +263,270 @@ class _OverviewPage(QWidget):
def updateProjectData(self) -> None:
"""Load information about the project."""
project = SHARED.project
project.updateWordCounts()
wcNovel, wcNotes = project.data.currCounts
self.projName.setText(project.data.name)
self.projWords.setText(f"{project.index.getNovelWordCount():n}")
self.projRevisions.setText(f"{project.data.saveCount:n}")
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
# 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"
"Well, not really. She's still awesome though.\n"
))
# Whole document
cC, wC, pC = index.getCounts(nHandle)
assert cC == 152
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Extract stats
assert index.getNovelWordCount(activeOnly=False) == 43
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=True) == [0, 1, 2, 3, 0]