diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index f58145f9..7f1598b6 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -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 diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 8e50c30e..c48aaa13 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -53,10 +53,6 @@ VALID_MAP: dict[str, set[str]] = { "GuiProjectSettings": { "winWidth", "winHeight", "replaceColW", "statusColW", "importColW", }, - "GuiProjectDetails": { - "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", - "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", - }, "GuiWordList": {"winWidth", "winHeight"}, "GuiNovelView": {"lastCol", "lastColSize"}, "GuiBuildSettings": { @@ -71,7 +67,12 @@ VALID_MAP: dict[str, set[str]] = { }, "GuiDocViewerPanel": { "colWidths", "hideInactive", - } + }, + "GuiNovelDetails": { + "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", + "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", + "novelRoot", + }, } diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 15b54660..a0c43611 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NHelpLabel +from novelwriter.extensions.configlayout import NColourLabel logger = logging.getLogger(__name__) @@ -54,9 +54,10 @@ class GuiDocMerge(QDialog): self._data = {} self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) - self.helpLabel = NHelpLabel(self.tr( - "Drag and drop items to change the order, or uncheck to exclude." - ), SHARED.theme.helpText) + self.helpLabel = NColourLabel( + self.tr("Drag and drop items to change the order, or uncheck to exclude."), + SHARED.theme.helpText, parent=self, wrap=True + ) iPx = SHARED.theme.baseIconSize hSp = CONFIG.pxInt(12) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 602b860c..b8add4c0 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NHelpLabel +from novelwriter.extensions.configlayout import NColourLabel logger = logging.getLogger(__name__) @@ -58,9 +58,9 @@ class GuiDocSplit(QDialog): self.setWindowTitle(self.tr("Split Document")) self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) - self.helpLabel = NHelpLabel( + self.helpLabel = NColourLabel( self.tr("Select the maximum level to split into files."), - SHARED.theme.helpText + SHARED.theme.helpText, parent=self, wrap=True ) # Values diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 32769451..5ece68b1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,11 +26,11 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence, QPalette +from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox, - QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit, + QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLineEdit, QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp ) @@ -38,7 +38,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.constants import nwConst, nwUnicode from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NScrollableForm +from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) @@ -58,22 +58,11 @@ class GuiPreferences(QDialog): self.resize(*CONFIG.preferencesWinSize) # Title - font = self.font() - font.setPointSizeF(1.25*SHARED.theme.fontPointSize) - - palette = self.palette() - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.helpText) - - self.titleLabel = QLabel(self.tr("Preferences"), self) - self.titleLabel.setFont(font) - self.titleLabel.setPalette(palette) + self.titleLabel = NColourLabel( + self.tr("Preferences"), SHARED.theme.helpText, parent=self, scale=1.25 + ) self.titleLabel.setIndent(CONFIG.pxInt(4)) - # SideBar - self.sidebar = NPagedSideBar(self) - self.sidebar.setLabelColor(SHARED.theme.helpText) - self.sidebar.buttonClicked.connect(self._sidebarClicked) - # Search Box self.searchText = QLineEdit(self) self.searchText.setPlaceholderText(self.tr("Search")) @@ -83,10 +72,10 @@ class GuiPreferences(QDialog): ) self.searchAction.triggered.connect(self._gotoSearch) - self.searchBox = QHBoxLayout() - self.searchBox.addWidget(self.titleLabel) - self.searchBox.addStretch(1) - self.searchBox.addWidget(self.searchText, 1) + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.buttonClicked.connect(self._sidebarClicked) # Form self.mainForm = NScrollableForm(self) @@ -101,6 +90,11 @@ class GuiPreferences(QDialog): self.buttonBox.clicked.connect(self._dialogButtonClicked) # Assemble + self.searchBox = QHBoxLayout() + self.searchBox.addWidget(self.titleLabel) + self.searchBox.addStretch(1) + self.searchBox.addWidget(self.searchText, 1) + self.mainBox = QHBoxLayout() self.mainBox.addWidget(self.sidebar) self.mainBox.addWidget(self.mainForm) diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py deleted file mode 100644 index a6e12163..00000000 --- a/novelwriter/dialogs/projdetails.py +++ /dev/null @@ -1,518 +0,0 @@ -""" -novelWriter – GUI Project Details -================================= - -File History: -Created: 2021-01-03 [1.1rc1] GuiProjectDetails - -This file is a part of novelWriter -Copyright 2018–2024, 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 . -""" -from __future__ import annotations - -import math -import logging - -from PyQt5.QtGui import QCloseEvent, QFont -from PyQt5.QtCore import Qt, QSize, pyqtSlot -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: QWidget) -> None: - 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.rejected.connect(self.close) - self.rejected.connect(self.close) - self.addControls(self.buttonBox) - - logger.debug("Ready: GuiProjectDetails") - - return - - def __del__(self) -> None: # pragma: no cover - logger.debug("Delete: GuiProjectDetails") - return - - def updateValues(self) -> None: - """Set all the values of the pages.""" - self.tabMain.updateValues() - self.tabContents.updateValues() - return - - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - self._saveGuiSettings() - event.accept() - self.deleteLater() - return - - ## - # Internal Functions - ## - - def _saveGuiSettings(self) -> None: - """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() - - logger.debug("Saving State: GuiProjectDetails") - 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: QWidget) -> None: - 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("%s:" % self.tr("Words")) - self.wordCountVal = QLabel("") - - self.chapCountLbl = QLabel("%s:" % self.tr("Chapters")) - self.chapCountVal = QLabel("") - - self.sceneCountLbl = QLabel("%s:" % self.tr("Scenes")) - self.sceneCountVal = QLabel("") - - self.revCountLbl = QLabel("%s:" % self.tr("Revisions")) - self.revCountVal = QLabel("") - - self.editTimeLbl = QLabel("%s:" % 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("%s:" % 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: QWidget) -> None: - 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("%s" % 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) -> 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 - - def updateValues(self) -> None: - """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: str | None) -> None: - """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: str) -> None: - """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) -> 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._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 diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 54ecb3c8..c71036be 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -3,7 +3,7 @@ novelWriter – Custom Widget: Config Layout ========================================== File History: -Created: 2020-05-03 [0.4.5] NConfigLayout, NHelpLabel +Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel Created: 2023-05-23 [2.1b1] NSimpleLayout Created: 2024-01-08 [2.3b1] NScrollableForm @@ -40,6 +40,10 @@ LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop class NScrollableForm(QScrollArea): + """Extension: Scrollable Form Widget + + A custom widget that creates a form within a scrollable area. + """ def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -48,7 +52,7 @@ class NScrollableForm(QScrollArea): self._first = True self._sections: dict[int, QLabel] = {} - self._editable: dict[str, NHelpLabel] = {} + self._editable: dict[str, NColourLabel] = {} self._index: dict[str, QWidget] = {} self._layout = QVBoxLayout() @@ -130,7 +134,7 @@ class NScrollableForm(QScrollArea): qLabel.setBuddy(widget) if helpText: - qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) qHelp.setIndent(mPx) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) @@ -193,14 +197,6 @@ class NConfigLayout(QGridLayout): self._fontScale = scale return - def setHelpText(self, row: int, text: str) -> None: - """Set the text for the help label.""" - if row in self._itemMap: - qHelp = self._itemMap[row][1] - if isinstance(qHelp, NHelpLabel): - qHelp.setText(text) - return - ## # Class Methods ## @@ -226,7 +222,7 @@ class NConfigLayout(QGridLayout): qHelp = None if helpText is not None: - qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) qHelp.setIndent(wSp) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) @@ -306,17 +302,9 @@ class NSimpleLayout(QGridLayout): wSp = CONFIG.pxInt(8) qLabel = QLabel(label) qLabel.setIndent(wSp) - self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) - - if isinstance(widget, QLineEdit): - qLayout = QHBoxLayout() - qLayout.addWidget(widget) - self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP) - else: - self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) - qLabel.setBuddy(widget) - + self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) + self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow+1, 1) self._nextRow += 1 @@ -326,10 +314,16 @@ class NSimpleLayout(QGridLayout): # END Class NSimpleLayout -class NHelpLabel(QLabel): +class NColourLabel(QLabel): + """Extension: A Coloured Label - def __init__(self, text: str, color: QColor, scale: float = FONT_SCALE) -> None: - super().__init__(text) + A custom widget that draws a label in a specific colour, and + optionally at a specific size, and word wrapped. + """ + + def __init__(self, text: str, color: QColor, parent: QWidget | None = None, + scale: float = FONT_SCALE, wrap: bool = False) -> None: + super().__init__(text, parent=parent) lblCol = self.palette() lblCol.setColor(QPalette.WindowText, color) @@ -339,9 +333,10 @@ class NHelpLabel(QLabel): lblFont.setPointSizeF(scale*lblFont.pointSizeF()) self.setFont(lblFont) - self.setWordWrap(True) - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + if wrap: + self.setWordWrap(True) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) return -# END Class NHelpLabel +# END Class NColourLabel diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index 035ab570..44e5bf23 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -3,7 +3,7 @@ novelWriter – Custom Widget: Novel Selector =========================================== File History: -Created: 2022-11-17 [2.0] +Created: 2022-11-17 [2.0] NovelSelector This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -43,6 +43,8 @@ class NovelSelector(QComboBox): super().__init__(parent=parent) self._blockSignal = False self._firstHandle = None + self._includeAll = False + self._listFormat = None self.currentIndexChanged.connect(self._indexChanged) return @@ -64,17 +66,29 @@ class NovelSelector(QComboBox): def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None: """Set the currently selected handle.""" - self._blockSignal = blockSignal - if tHandle is None: - index = self.count() - 1 - else: - index = self.findData(tHandle) - if index >= 0: + if (index := self.findData(tHandle) if tHandle else (self.count() - 1)) >= 0: + self._blockSignal = blockSignal self.setCurrentIndex(index) - self._blockSignal = False + self._blockSignal = False return - def updateList(self, includeAll: bool = False, prefix: str | None = None) -> None: + def setIncludeAll(self, value: bool) -> None: + """Set flag to add an "All Novel Folders" option.""" + self._includeAll = value + return + + def setListFormat(self, value: str | None) -> None: + """Set a format string for the list entries.""" + if value is None or "{0}" in value: + self._listFormat = value + return + + ## + # Public Slots + ## + + @pyqtSlot() + def refreshNovelList(self) -> None: """Rebuild the list of novel items.""" self._blockSignal = True self._firstHandle = None @@ -83,8 +97,8 @@ class NovelSelector(QComboBox): icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) handle = self.currentData() for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL): - if prefix: - name = prefix.format(nwItem.itemName) + if self._listFormat: + name = self._listFormat.format(nwItem.itemName) self.addItem(name, tHandle) else: name = nwItem.itemName @@ -92,7 +106,7 @@ class NovelSelector(QComboBox): if self._firstHandle is None: self._firstHandle = tHandle - if includeAll: + if self._includeAll: self.insertSeparator(self.count()) self.addItem(icon, self.tr("All Novel Folders"), "") diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 185b4d8a..082e8a2a 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -45,10 +45,9 @@ class NPagedSideBar(QToolBar): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self._buttons = [] - self._actions = [] self._labelCol = None self._spacerHeight = self.fontMetrics().height() // 2 + self._buttons: dict[int, _NPagedToolButton] = {} self._group = QButtonGroup(self) self._group.setExclusive(True) @@ -95,8 +94,7 @@ class NPagedSideBar(QToolBar): action = self.insertWidget(self._stretchAction, button) self._group.addButton(button, id=buttonId) - self._buttons.append(button) - self._actions.append(action) + self._buttons[buttonId] = button return action diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 189cd72d..279fce07 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -155,10 +155,10 @@ class GuiMainMenu(QMenuBar): self.aProjectSettings.setShortcut("Ctrl+Shift+,") self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog) - # Project > Project Details - self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details")) - self.aProjectDetails.setShortcut("Shift+F6") - self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog) + # Project > Novel Details + self.aNovelDetails = self.projMenu.addAction(self.tr("Novel Details")) + self.aNovelDetails.setShortcut("Shift+F6") + self.aNovelDetails.triggered.connect(self.mainGui.showNovelDetailsDialog) # Project > Separator self.projMenu.addSeparator() diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 1569262b..959ec7ae 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -207,9 +207,10 @@ class GuiNovelToolBar(QWidget): # Novel Selector selFont = self.font() selFont.setWeight(QFont.Weight.Bold) - self.novelPrefix = self.tr("Outline of {0}") + self.novelValue = NovelSelector(self) self.novelValue.setFont(selFont) + self.novelValue.setListFormat(self.tr("Outline of {0}")) self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) @@ -294,7 +295,7 @@ class GuiNovelToolBar(QWidget): "QComboBox {border-style: none; padding-left: 0;} " "QComboBox::drop-down {border-style: none}" ) - self.novelValue.updateList(prefix=self.novelPrefix) + self.novelValue.refreshNovelList() self.tbNovel.setVisible(self.novelValue.count() > 1) return @@ -307,7 +308,7 @@ class GuiNovelToolBar(QWidget): def buildNovelRootMenu(self) -> None: """Build the novel root menu.""" - self.novelValue.updateList(prefix=self.novelPrefix) + self.novelValue.refreshNovelList() self.tbNovel.setVisible(self.novelValue.count() > 1) return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index db066cee..cd441bae 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -220,6 +220,7 @@ class GuiOutlineToolBar(QToolBar): self.novelLabel.setContentsMargins(0, 0, mPx, 0) self.novelValue = NovelSelector(self) + self.novelValue.setIncludeAll(True) self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) @@ -258,7 +259,7 @@ class GuiOutlineToolBar(QToolBar): def updateTheme(self) -> None: """Update theme elements.""" self.setStyleSheet("QToolBar {border: 0px;}") - self.novelValue.updateList(includeAll=True) + self.novelValue.refreshNovelList() self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") @@ -266,7 +267,7 @@ class GuiOutlineToolBar(QToolBar): def populateNovelList(self) -> None: """Reload the content of the novel list.""" - self.novelValue.updateList(includeAll=True) + self.novelValue.refreshNovelList() return def setCurrentRoot(self, rootHandle: str | None) -> None: diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 92522542..d425ccf8 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -79,9 +79,9 @@ class GuiSideBar(QWidget): self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbDetails = QToolButton(self) - self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details"))) + self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) self.tbDetails.setIconSize(iconSize) - self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog) + self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog) self.tbStats = QToolButton(self) self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6717351a..ad29b741 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -53,11 +53,11 @@ from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.preferences import GuiPreferences -from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.dictionaries import GuiDictionaries +from novelwriter.tools.noveldetails import GuiNovelDetails from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.enum import ( @@ -825,10 +825,10 @@ class GuiMain(QMainWindow): return @pyqtSlot() - def showProjectDetailsDialog(self) -> None: - """Open the project details dialog.""" + def showNovelDetailsDialog(self) -> None: + """Open the novel details dialog.""" if SHARED.hasProject: - dialog = GuiProjectDetails(self) + dialog = GuiNovelDetails(self) dialog.setModal(True) dialog.show() dialog.raise_() diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py new file mode 100644 index 00000000..3fcd54cc --- /dev/null +++ b/novelwriter/tools/noveldetails.py @@ -0,0 +1,523 @@ +""" +novelWriter – GUI Novel Info +============================ + +File History: +Created: 2024-01-18 [2.3b1] GuiNovelDetails + +This file is a part of novelWriter +Copyright 2018–2024, 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 . +""" +from __future__ import annotations + +import math +import logging + +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import QSize, Qt, pyqtSlot +from PyQt5.QtWidgets import ( + QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout, + QHBoxLayout, QLabel, QSpinBox, QStackedWidget, 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.configlayout import NColourLabel +from novelwriter.extensions.pagedsidebar import NPagedSideBar +from novelwriter.extensions.novelselector import NovelSelector + +logger = logging.getLogger(__name__) + +HEADER_SIZE = 1.4 + + +class GuiNovelDetails(QDialog): + + PAGE_OVERVIEW = 1 + PAGE_CONTENTS = 2 + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + logger.debug("Create: GuiNovelDetails") + self.setObjectName("GuiNovelDetails") + self.setWindowTitle(self.tr("Novel Details")) + + wW = CONFIG.pxInt(500) + wH = CONFIG.pxInt(400) + options = SHARED.project.options + + self.setMinimumSize(wW, wH) + self.resize( + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", wW)), + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", wH)) + ) + + # Title + self.titleLabel = NColourLabel( + self.tr("Novel Details"), SHARED.theme.helpText, parent=self, scale=1.25 + ) + self.titleLabel.setIndent(CONFIG.pxInt(4)) + + # Novel Selector + self.novelSelector = NovelSelector(self) + self.novelSelector.refreshNovelList() + self.novelSelector.setHandle( + options.getString("GuiNovelDetails", "novelRoot", self.novelSelector.firstHandle or "") + ) + + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.addButton(self.tr("Overview"), self.PAGE_OVERVIEW) + self.sidebar.addButton(self.tr("Contents"), self.PAGE_CONTENTS) + self.sidebar.setSelected(self.PAGE_OVERVIEW) + self.sidebar.buttonClicked.connect(self._sidebarClicked) + + # 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) + self.buttonBox.rejected.connect(self.close) + + # Assemble + self.topBox = QHBoxLayout() + self.topBox.addWidget(self.titleLabel) + self.topBox.addStretch(1) + self.topBox.addWidget(self.novelSelector, 1) + + self.mainBox = QHBoxLayout() + self.mainBox.addWidget(self.sidebar) + self.mainBox.addWidget(self.mainStack) + self.mainBox.setContentsMargins(0, 0, 0, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.topBox) + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(CONFIG.pxInt(8)) + + 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 + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiNovelDetails") + return + + ## + # Methods + ## + + 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 + + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window and save settings.""" + self._saveSettings() + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot(int) + def _sidebarClicked(self, pageId: int) -> None: + """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 + + ## + # Internal Functions + ## + + def _saveSettings(self) -> None: + """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 + + +class _OverviewPage(QWidget): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + mPx = CONFIG.pxInt(8) + sPx = CONFIG.pxInt(16) + hPx = CONFIG.pxInt(24) + vPx = CONFIG.pxInt(4) + + # Project Info + 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("{0}".format(self.tr("Name")), self.projName) + self.projForm.addRow("{0}".format(self.tr("Revisions")), self.projRevisions) + self.projForm.addRow("{0}".format(self.tr("Editing Time")), self.projEditTime) + self.projForm.addRow("{0}".format(self.tr("Word Count")), self.projWords) + self.projForm.addRow("\u2026 {0}".format(self.tr("In Novels")), self.projNovels) + self.projForm.addRow("\u2026 {0}".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("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + ) + + self.novelName = QLabel("", self) + self.novelWords = QLabel("", self) + self.novelChapters = QLabel("", self) + self.novelScenes = QLabel("", self) + + self.novelForm = QFormLayout() + self.novelForm.addRow("{0}".format(self.tr("Name")), self.novelName) + self.novelForm.addRow("{0}".format(self.tr("Word Count")), self.novelWords) + self.novelForm.addRow("{0}".format(self.tr("Chapters")), self.novelChapters) + self.novelForm.addRow("{0}".format(self.tr("Scenes")), self.novelScenes) + self.novelForm.setContentsMargins(mPx, 0, 0, 0) + self.novelForm.setHorizontalSpacing(hPx) + self.novelForm.setVerticalSpacing(vPx) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.projLabel) + self.outerBox.addLayout(self.projForm) + self.outerBox.addWidget(self.novelLabel) + self.outerBox.addLayout(self.novelForm) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(sPx) + self.outerBox.addStretch(1) + + self.setLayout(self.outerBox) + + return + + ## + # Methods + ## + + 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.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 + + ## + # 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 diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 0329434e..385f7bd2 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -25,7 +25,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path from datetime import datetime @@ -48,9 +47,6 @@ from novelwriter.constants import nwUnicode from novelwriter.core.coretools import ProjectBuilder from novelwriter.extensions.switch import NSwitch -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -58,8 +54,8 @@ class GuiWelcome(QDialog): openProjectRequest = pyqtSignal(Path) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiWelcome") self.setObjectName("GuiWelcome") diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e9dee3f..44836349 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -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] diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py index 7458c746..fd291d08 100644 --- a/tests/test_core/test_core_options.py +++ b/tests/test_core/test_core_options.py @@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI): assert options.setValue("GuiProjectSettings", "winWidth", 100) is True # Set some values of different types - assert options.setValue("GuiProjectDetails", "winWidth", 100) is True - assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True - assert options.setValue("GuiProjectDetails", "clearDouble", True) is True + assert options.setValue("GuiNovelDetails", "winWidth", 100) is True + assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True + assert options.setValue("GuiNovelDetails", "clearDouble", True) is True assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True # Generic get, doesn't check type - assert options.getValue("GuiProjectDetails", "winWidth", None) == 100 - assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34 - assert options.getValue("GuiProjectDetails", "clearDouble", None) is True - assert options.getValue("GuiProjectDetails", "mockItem", None) is None + assert options.getValue("GuiNovelDetails", "winWidth", None) == 100 + assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34 + assert options.getValue("GuiNovelDetails", "clearDouble", None) is True + assert options.getValue("GuiNovelDetails", "mockItem", None) is None # Get type-specific - assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore - assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore - assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore - assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore - assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore - assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore + assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore + assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore + assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore + assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore + assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore + assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden # Get from non-existent groups diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index c4c2e590..408865c7 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -22,9 +22,11 @@ from __future__ import annotations import pytest +from tools import getGuiItem + from PyQt5.QtGui import QFontDatabase, QKeyEvent from PyQt5.QtCore import QEvent, Qt -from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog +from PyQt5.QtWidgets import QAction, QDialogButtonBox, QFileDialog, QFontDialog from novelwriter import CONFIG, SHARED from novelwriter.constants import nwConst, nwUnicode @@ -38,9 +40,13 @@ KEY_DELAY = 1 def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): """Test the preferences dialog loading.""" monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) + monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) # Load GUI with standard values - prefs = GuiPreferences(nwGUI) + nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) + prefs = getGuiItem("GuiPreferences") + assert isinstance(prefs, GuiPreferences) prefs.show() # Check Languages @@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): # Check Navigation vBar = prefs.mainForm.verticalScrollBar() old = -1 - with qtbot.waitSignal(vBar.valueChanged) as value: - prefs.sidebar.button(0).click() - assert value.args[0] > old - old = value.args[0] with qtbot.waitSignal(vBar.valueChanged) as value: prefs.sidebar.button(1).click() assert value.args[0] > old @@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): prefs.sidebar.button(2).click() assert value.args[0] > old old = value.args[0] + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs.sidebar.button(3).click() + assert value.args[0] > old + old = value.args[0] # Check Search prefs.searchText.setText("Display language") diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py deleted file mode 100644 index 3d68f9d7..00000000 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -novelWriter – Project Details Dialog Class Tester -================================================= - -This file is a part of novelWriter -Copyright 2018–2024, 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 . -""" -from __future__ import annotations - -import pytest - -from tools import getGuiItem - -from PyQt5.QtWidgets import QAction - -from novelwriter import SHARED -from novelwriter.dialogs.projdetails import GuiProjectDetails - - -@pytest.mark.gui -def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): - """Test the project details dialog. - """ - # Create a project to work on - assert nwGUI.openProject(prjLipsum) - assert nwGUI.rebuildIndex(beQuiet=True) - qtbot.wait(100) - - # Open the Writing Stats dialog - nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000) - - projDet = getGuiItem("GuiProjectDetails") - assert isinstance(projDet, GuiProjectDetails) - - # Overview Page - # ============= - - assert projDet.tabMain.bookTitle.text() == "Lorem Ipsum" - assert projDet.tabMain.projName.text()[-11:] == "Lorem Ipsum" - assert projDet.tabMain.bookAuthors.text()[-10:] == "lipsum.com" - - assert projDet.tabMain.wordCountVal.text() == f"{3000:n}" - assert projDet.tabMain.chapCountVal.text() == f"{3:n}" - assert projDet.tabMain.sceneCountVal.text() == f"{5:n}" - assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}" - - assert projDet.tabMain.projPathVal.text() == str(prjLipsum) - - # Contents Page - # ============= - - tocTab = projDet.tabContents - tocTree = tocTab.tocTree - assert tocTree.topLevelItemCount() == 7 - assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore - assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore - assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore - assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore - assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore - assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore - - # Count Pages - tocTab.wpValue.setValue(100) - tocTab.poValue.setValue(4) - tocTab.dblValue.setChecked(False) - tocTab._populateTree() - - thePages = ["1", "2", "1", "1", "11", "17", "0"] - thePage = ["i", "ii", "1", "2", "3", "14", "31"] - for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore - - tocTab.poValue.setValue(5) - tocTab.dblValue.setChecked(True) - tocTab._populateTree() - - thePages = ["2", "2", "2", "2", "12", "18", "0"] - thePage = ["i", "iii", "1", "3", "5", "17", "35"] - for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore - - # Re-populate - assert tocTab._currentRoot is None - tocTab._novelValueChanged("7a992350f3eb6") # Not a root - assert tocTab._currentRoot == "b3643d0f92e32" # The actual novel root - - # qtbot.stop() - - # Clean Up - projDet.close() - nwGUI.closeMain() - -# END Test testDlgProjDetails_Dialog diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py new file mode 100644 index 00000000..c8925e53 --- /dev/null +++ b/tests/test_tools/test_tools_noveldetails.py @@ -0,0 +1,136 @@ +""" +novelWriter – Novel Details Tool Tester +======================================= + +This file is a part of novelWriter +Copyright 2018–2024, 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 . +""" +from __future__ import annotations + +import pytest + +from tools import getGuiItem + +from PyQt5.QtWidgets import QAction + +from novelwriter import SHARED +from novelwriter.enum import nwItemClass +from novelwriter.tools.noveldetails import GuiNovelDetails + + +@pytest.mark.gui +def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): + """Test the Novel Details main dialog.""" + nwGUI.openProject(prjLipsum) + nHandle = "b3643d0f92e32" + + # Add a second Novel folder + project = SHARED.project + secondText = "#! Second\n\n" + "\n\n".join(ipsumText) + sHandle = project.newRoot(nwItemClass.NOVEL, "Second") + dHandle = project.newFile("Document", sHandle) + project.storage.getDocument(dHandle).writeDocument(secondText) + project.index.reIndexHandle(dHandle) + nwGUI.projView.projTree.revealNewTreeItem(sHandle) + nwGUI.projView.projTree.revealNewTreeItem(dHandle) + + # Create the dialog + nwGUI.mainMenu.aNovelDetails.activate(QAction.ActionEvent.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiNovelDetails") is not None, timeout=1000) + details = getGuiItem("GuiNovelDetails") + assert isinstance(details, GuiNovelDetails) + + # Overview Page + # ============= + overview = details.overviewPage + + # The selector should default to the first entry + assert details.novelSelector.handle == nHandle + + # Check project data + assert overview.projName.text() == "Lorem Ipsum" + assert overview.projWords.text() == f"{4376:n}" + assert overview.projNovels.text() == f"{3638:n}" + assert overview.projNotes.text() == f"{738:n}" + assert overview.projRevisions.text() != "" + assert overview.projEditTime.text() != "" + + # Check novel data for "Novel" + assert overview.novelName.text() == "Novel" + assert overview.novelWords.text() == f"{3000:n}" + assert overview.novelChapters.text() == f"{3:n}" + assert overview.novelScenes.text() == f"{5:n}" + + # Check novel data for "Second" + details.novelSelector.setHandle(sHandle, blockSignal=False) + assert overview.novelName.text() == "Second" + assert overview.novelWords.text() == f"{529:n}" + assert overview.novelChapters.text() == f"{0:n}" + assert overview.novelScenes.text() == f"{0:n}" + + # Contents Page + # ============= + details.novelSelector.setHandle(nHandle, blockSignal=False) + details.sidebar.button(details.PAGE_CONTENTS).click() + assert details.mainStack.currentIndex() == 1 + contents = details.contentsPage + + # Check defaults + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]] + page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Change Settings + contents.poValue.setValue(7) + contents.wpValue.setValue(50) + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]] + page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Turn off use odd pages + contents.dblValue.setChecked(False) + contents.poValue.setValue(0) + contents.wpValue.setValue(100) + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]] + page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Revert to Overview page + details.sidebar.button(details.PAGE_OVERVIEW).click() + assert details.mainStack.currentIndex() == 0 + + # qtbot.stop() + details.close() + +# END Test testToolNovelDetails_Main diff --git a/tests/tools.py b/tests/tools.py index 6bc21966..51cdcc19 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -156,20 +156,23 @@ def cleanProject(path: str | Path): return -def buildTestProject(obj, projPath): +def buildTestProject(obj: object, projPath: Path) -> None: """Build a standard test project in projPath using the project object as the parent. """ from novelwriter.enum import nwItemClass + from novelwriter.guimain import GuiMain from novelwriter.core.project import NWProject if isinstance(obj, NWProject): nwGUI = None project = obj - else: + elif isinstance(obj, GuiMain): from novelwriter import SHARED nwGUI = obj project = SHARED.project + else: + return project.storage.createNewProject(projPath) project.setDefaultStatusImport() @@ -181,27 +184,27 @@ def buildTestProject(obj, projPath): # Creating a minimal project with a few root folders and a # single chapter folder with a single file. - xHandle = {} - xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel") - xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot") - xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters") - xHandle[4] = project.newRoot(nwItemClass.WORLD, "World") - xHandle[5] = project.newFile("Title Page", xHandle[1]) - xHandle[6] = project.newFolder("New Chapter", xHandle[1]) - xHandle[7] = project.newFile("New Chapter", xHandle[6]) - xHandle[8] = project.newFile("New Scene", xHandle[6]) + nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel") + project.newRoot(nwItemClass.PLOT, "Plot") + project.newRoot(nwItemClass.CHARACTER, "Characters") + project.newRoot(nwItemClass.WORLD, "World") - aDoc = project.storage.getDocument(xHandle[5]) + tdHandle = project.newFile("Title Page", nrHandle) + cfHandle = project.newFolder("New Chapter", nrHandle) or "" + cdHandle = project.newFile("New Chapter", cfHandle) + sdHandle = project.newFile("New Scene", cfHandle) + + aDoc = project.storage.getDocument(tdHandle) aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n") - project.index.reIndexHandle(xHandle[5]) + project.index.reIndexHandle(tdHandle) - aDoc = project.storage.getDocument(xHandle[7]) + aDoc = project.storage.getDocument(cdHandle) aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter")) - project.index.reIndexHandle(xHandle[7]) + project.index.reIndexHandle(cdHandle) - aDoc = project.storage.getDocument(xHandle[8]) + aDoc = project.storage.getDocument(sdHandle) aDoc.writeDocument("### %s\n\n" % project.tr("New Scene")) - project.index.reIndexHandle(xHandle[8]) + project.index.reIndexHandle(sdHandle) project.session.startSession() project.setProjectChanged(True)