Files
novelWriter/novelwriter/dialogs/projdetails.py
T

526 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter GUI Project Details
=================================
File History:
Created: 2021-01-03 [1.1rc1]
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import math
import logging
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
QAbstractItemView, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel,
QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog
from novelwriter.extensions.novelselector import NovelSelector
logger = logging.getLogger(__name__)
class GuiProjectDetails(NPagedDialog):
def __init__(self, parent):
super().__init__(parent=parent)
logger.debug("Create: GuiProjectDetails")
self.setObjectName("GuiProjectDetails")
self.setWindowTitle(self.tr("Project Details"))
wW = CONFIG.pxInt(600)
wH = CONFIG.pxInt(400)
pOptions = SHARED.project.options
self.setMinimumWidth(wW)
self.setMinimumHeight(wH)
self.resize(
CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)),
CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
)
self.tabMain = GuiProjectDetailsMain(self)
self.tabContents = GuiProjectDetailsContents(self)
self.addTab(self.tabMain, self.tr("Overview"))
self.addTab(self.tabContents, self.tr("Contents"))
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.button(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
logger.debug("Ready: GuiProjectDetails")
return
def __del__(self): # pragma: no cover
logger.debug("Delete: GuiProjectDetails")
return
def updateValues(self):
"""Set all the values of the pages.
"""
self.tabMain.updateValues()
self.tabContents.updateValues()
return
##
# Slots
##
def _doClose(self):
"""Save settings and close the dialog.
"""
self._saveGuiSettings()
self.close()
return
##
# Internal Functions
##
def _saveGuiSettings(self):
"""Save GUI settings.
"""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
cColWidth = self.tabContents.getColumnSizes()
widthCol0 = CONFIG.rpxInt(cColWidth[0])
widthCol1 = CONFIG.rpxInt(cColWidth[1])
widthCol2 = CONFIG.rpxInt(cColWidth[2])
widthCol3 = CONFIG.rpxInt(cColWidth[3])
widthCol4 = CONFIG.rpxInt(cColWidth[4])
wordsPerPage = self.tabContents.wpValue.value()
countFrom = self.tabContents.poValue.value()
clearDouble = self.tabContents.dblValue.isChecked()
pOptions = SHARED.project.options
pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1)
pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2)
pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3)
pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4)
pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
pOptions.setValue("GuiProjectDetails", "countFrom", countFrom)
pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble)
return
# END Class GuiProjectDetails
class GuiProjectDetailsMain(QWidget):
def __init__(self, parent):
super().__init__(parent=parent)
fPx = SHARED.theme.fontPixelSize
fPt = SHARED.theme.fontPointSize
vPx = CONFIG.pxInt(4)
hPx = CONFIG.pxInt(12)
# Header
# ======
self.bookTitle = QLabel("")
bookFont = self.bookTitle.font()
bookFont.setPointSizeF(2.2*fPt)
bookFont.setWeight(QFont.Bold)
self.bookTitle.setFont(bookFont)
self.bookTitle.setAlignment(Qt.AlignHCenter)
self.bookTitle.setWordWrap(True)
self.projName = QLabel("")
workFont = self.projName.font()
workFont.setPointSizeF(0.8*fPt)
workFont.setItalic(True)
self.projName.setFont(workFont)
self.projName.setAlignment(Qt.AlignHCenter)
self.projName.setWordWrap(True)
self.bookAuthors = QLabel("")
authFont = self.bookAuthors.font()
authFont.setPointSizeF(1.2*fPt)
self.bookAuthors.setFont(authFont)
self.bookAuthors.setAlignment(Qt.AlignHCenter)
self.bookAuthors.setWordWrap(True)
# Stats
# =====
self.wordCountLbl = QLabel("<b>%s:</b>" % self.tr("Words"))
self.wordCountVal = QLabel("")
self.chapCountLbl = QLabel("<b>%s:</b>" % self.tr("Chapters"))
self.chapCountVal = QLabel("")
self.sceneCountLbl = QLabel("<b>%s:</b>" % self.tr("Scenes"))
self.sceneCountVal = QLabel("")
self.revCountLbl = QLabel("<b>%s:</b>" % self.tr("Revisions"))
self.revCountVal = QLabel("")
self.editTimeLbl = QLabel("<b>%s:</b>" % self.tr("Editing Time"))
self.editTimeVal = QLabel("")
self.statsGrid = QGridLayout()
self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.setHorizontalSpacing(hPx)
self.statsGrid.setVerticalSpacing(vPx)
# Meta
# ====
self.projPathLbl = QLabel("<b>%s:</b>" % self.tr("Path"))
self.projPathVal = QLineEdit()
self.projPathVal.setReadOnly(True)
self.projPathBox = QHBoxLayout()
self.projPathBox.addWidget(self.projPathLbl)
self.projPathBox.addWidget(self.projPathVal)
self.projPathBox.setSpacing(hPx)
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addSpacing(fPx)
self.outerBox.addWidget(self.bookTitle)
self.outerBox.addWidget(self.projName)
self.outerBox.addWidget(self.bookAuthors)
self.outerBox.addSpacing(2*fPx)
self.outerBox.addLayout(self.statsGrid)
self.outerBox.addSpacing(fPx)
self.outerBox.addStretch(1)
self.outerBox.addLayout(self.projPathBox)
self.setLayout(self.outerBox)
return
def updateValues(self) -> None:
"""Set all the values."""
project = SHARED.project
pIndex = project.index
hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount()
edTime = project.currentEditTime
self.bookTitle.setText(project.data.title or project.data.name)
self.projName.setText(self.tr("Project: {0}").format(project.data.name))
self.bookAuthors.setText(self.tr("By {0}").format(project.data.author))
self.wordCountVal.setText(f"{nwCount:n}")
self.chapCountVal.setText(f"{hCounts[2]:n}")
self.sceneCountVal.setText(f"{hCounts[3]:n}")
self.revCountVal.setText(f"{project.data.saveCount:n}")
self.editTimeVal.setText(formatTime(edTime))
self.projPathVal.setText(str(project.storage.storagePath))
return
# END Class GuiProjectDetailsMain
class GuiProjectDetailsContents(QWidget):
C_TITLE = 0
C_WORDS = 1
C_PAGES = 2
C_PAGE = 3
C_PROG = 4
def __init__(self, parent):
super().__init__(parent=parent)
# Internal
self._theToC = []
self._currentRoot = None
iPx = SHARED.theme.baseIconSize
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
pOptions = SHARED.project.options
# Header
# ======
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
self.novelValue = NovelSelector(self)
self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
self.headBox = QHBoxLayout()
self.headBox.addWidget(self.tocLabel)
self.headBox.addWidget(self.novelValue)
# Contents Tree
# =============
self.tocTree = QTreeWidget()
self.tocTree.setIconSize(QSize(iPx, iPx))
self.tocTree.setIndentation(0)
self.tocTree.setColumnCount(6)
self.tocTree.setSelectionMode(QAbstractItemView.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.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
treeHeader = self.tocTree.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx)
wCol0 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200))
wCol1 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60))
wCol2 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60))
wCol3 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60))
wCol4 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "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 = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True)
wordsHelp = (
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
)
offsetHelp = (
self.tr("Start counting page numbers from this page.")
)
dblHelp = (
self.tr("Assume a new chapter or partition always start on an odd numbered page.")
)
self.wpLabel = QLabel(self.tr("Words per page"))
self.wpLabel.setToolTip(wordsHelp)
self.wpValue = QSpinBox()
self.wpValue.setMinimum(10)
self.wpValue.setMaximum(1000)
self.wpValue.setSingleStep(10)
self.wpValue.setValue(wordsPerPage)
self.wpValue.setToolTip(wordsHelp)
self.wpValue.valueChanged.connect(self._populateTree)
self.poLabel = QLabel(self.tr("Count pages from"))
self.poLabel.setToolTip(offsetHelp)
self.poValue = QSpinBox()
self.poValue.setMinimum(1)
self.poValue.setMaximum(9999)
self.poValue.setSingleStep(1)
self.poValue.setValue(countFrom)
self.poValue.setToolTip(offsetHelp)
self.poValue.valueChanged.connect(self._populateTree)
self.dblLabel = QLabel(self.tr("Clear double pages"))
self.dblLabel.setToolTip(dblHelp)
self.dblValue = NSwitch(self, 2*iPx, iPx)
self.dblValue.setChecked(clearDouble)
self.dblValue.setToolTip(dblHelp)
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.addLayout(self.headBox)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
self.setLayout(self.outerBox)
return
def getColumnSizes(self):
"""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
def updateValues(self):
"""Populate the tree.
"""
self._currentRoot = None
self.novelValue.updateList()
self.novelValue.setHandle(self.novelValue.firstHandle)
self._prepareData(self.novelValue.firstHandle)
self._populateTree()
return
##
# Internal Functions
##
def _prepareData(self, rootHandle):
"""Extract the information from the project index.
"""
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2)
self._theToC.append(("", 0, self.tr("END"), 0))
return
##
# Slots
##
@pyqtSlot(str)
def _novelValueChanged(self, tHandle):
"""Refresh the tree with another root item.
"""
if tHandle != self._currentRoot:
self._prepareData(tHandle)
self._populateTree()
self._currentRoot = self.novelValue.handle
return
@pyqtSlot()
def _populateTree(self):
"""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._theToC:
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
# END Class GuiProjectDetailsContents