From e37767589759bf6afdeca99093d200b9593ee1e4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Jan 2021 18:14:00 +0100 Subject: [PATCH 01/15] Added basic project details dialog --- nw/gui/__init__.py | 2 + nw/gui/projdetails.py | 103 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 nw/gui/projdetails.py diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 6b71867d..aa7455b5 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -13,6 +13,7 @@ from nw.gui.noveltree import GuiNovelTree from nw.gui.outline import GuiOutline from nw.gui.outlinedetails import GuiOutlineDetails from nw.gui.preferences import GuiPreferences +from nw.gui.projdetails import GuiProjectDetails from nw.gui.projload import GuiProjectLoad from nw.gui.projsettings import GuiProjectSettings from nw.gui.projtree import GuiProjectTree @@ -37,6 +38,7 @@ __all__ = [ "GuiOutline", "GuiOutlineDetails", "GuiPreferences", + "GuiProjectDetails", "GuiProjectLoad", "GuiProjectSettings", "GuiProjectTree", diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py new file mode 100644 index 00000000..5de7950b --- /dev/null +++ b/nw/gui/projdetails.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Project Details + + novelWriter – GUI Project Details +=================================== + Class holding the project details dialog + + File History: + Created: 2021-01-03 [1.0a0] + + This file is a part of novelWriter + Copyright 2018–2021, 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 . +""" + +import nw +import logging + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QWidget, QDialogButtonBox, QTreeWidget, QTreeWidgetItem +) + +from nw.gui.custom import PagedDialog, QConfigLayout + +logger = logging.getLogger(__name__) + +class GuiProjectDetails(PagedDialog): + + def __init__(self, theParent, theProject): + PagedDialog.__init__(self, theParent) + + logger.debug("Initialising GuiProjectDetails ...") + self.setObjectName("GuiProjectDetails") + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.optState = theProject.optState + + self.setWindowTitle("Project Details") + + wW = self.mainConf.pxInt(570) + wH = self.mainConf.pxInt(375) + + self.setMinimumWidth(wW) + self.setMinimumHeight(wH) + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winWidth", wW)), + self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH)) + ) + + # self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) + + # self.addTab(self.tabMain, "Settings") + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok) + self.buttonBox.rejected.connect(self._doClose) + self.addControls(self.buttonBox) + + logger.debug("GuiProjectDetails initialisation complete") + + 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 = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) + + self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) + self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) + + return + +# END Class GuiProjectDetails From 7e65aea6d9a406fbc72dfcae79766ac86fea8f56 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Jan 2021 21:46:53 +0100 Subject: [PATCH 02/15] Connected new dialog to menu --- nw/core/options.py | 6 +++++- nw/gui/mainmenu.py | 7 +++++++ nw/gui/projdetails.py | 9 +++------ nw/guimain.py | 17 +++++++++++++++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index db94ae45..d35a26f5 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -87,7 +87,11 @@ class OptionState(): "winWidth", "winHeight", "replaceColW", - } + }, + "GuiProjectDetails": { + "winWidth", + "winHeight", + }, } return diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 554eca52..44348af6 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -219,6 +219,13 @@ class GuiMainMenu(QMenuBar): self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) self.projMenu.addAction(self.aProjectSettings) + # Project > Project Details + self.aProjectDetails = QAction("Project Details", self) + self.aProjectDetails.setStatusTip("Project details") + self.aProjectDetails.setShortcut("Ctrl+Shift+E") + self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) + self.projMenu.addAction(self.aProjectDetails) + # Project > Separator self.projMenu.addSeparator() diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 5de7950b..57610284 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -28,12 +28,9 @@ import nw import logging -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import ( - QWidget, QDialogButtonBox, QTreeWidget, QTreeWidgetItem -) +from PyQt5.QtWidgets import QDialogButtonBox -from nw.gui.custom import PagedDialog, QConfigLayout +from nw.gui.custom import PagedDialog logger = logging.getLogger(__name__) @@ -66,7 +63,7 @@ class GuiProjectDetails(PagedDialog): # self.addTab(self.tabMain, "Settings") - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) diff --git a/nw/guimain.py b/nw/guimain.py index 8640aa52..7eee016a 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -43,8 +43,8 @@ from nw.gui import ( GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, - GuiPreferences, GuiProjectLoad, GuiProjectSettings, GuiProjectTree, - GuiProjectWizard, GuiTheme, GuiWritingStats + GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, + GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWritingStats ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst @@ -907,6 +907,19 @@ class GuiMain(QMainWindow): return + def showProjectDetailsDialog(self): + """Open the project details dialog. + """ + if not self.hasProject: + logger.error("No project open") + return + + dlgDetails = GuiProjectDetails(self, self.theProject) + dlgDetails.setModal(False) + dlgDetails.show() + + return + def showBuildProjectDialog(self): """Open the build project dialog. """ From f2b295290915a170f511ce45f50ad69bca9a9128 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 9 Jan 2021 23:52:07 +0100 Subject: [PATCH 03/15] Added heading level 0 icon --- .../icons/typicons_colour_dark/heading0.svg | 31 +++++++++++++++++++ .../icons/typicons_colour_dark/icons.conf | 1 + .../icons/typicons_colour_light/heading0.svg | 31 +++++++++++++++++++ .../icons/typicons_colour_light/icons.conf | 1 + .../icons/typicons_grey_dark/heading0.svg | 31 +++++++++++++++++++ nw/assets/icons/typicons_grey_dark/icons.conf | 1 + .../icons/typicons_grey_light/heading0.svg | 31 +++++++++++++++++++ .../icons/typicons_grey_light/icons.conf | 1 + nw/gui/theme.py | 1 + 9 files changed, 129 insertions(+) create mode 100644 nw/assets/icons/typicons_colour_dark/heading0.svg create mode 100644 nw/assets/icons/typicons_colour_light/heading0.svg create mode 100644 nw/assets/icons/typicons_grey_dark/heading0.svg create mode 100644 nw/assets/icons/typicons_grey_light/heading0.svg diff --git a/nw/assets/icons/typicons_colour_dark/heading0.svg b/nw/assets/icons/typicons_colour_dark/heading0.svg new file mode 100644 index 00000000..65ca642c --- /dev/null +++ b/nw/assets/icons/typicons_colour_dark/heading0.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/typicons_colour_dark/icons.conf b/nw/assets/icons/typicons_colour_dark/icons.conf index d4671820..5d4c0f2e 100644 --- a/nw/assets/icons/typicons_colour_dark/icons.conf +++ b/nw/assets/icons/typicons_colour_dark/icons.conf @@ -29,6 +29,7 @@ cls_archive = delete.svg cls_trash = trash.svg proj_document = document-text.svg proj_folder = folder.svg +doc_h0 = heading0.svg doc_h1 = heading1.svg doc_h2 = heading2.svg doc_h3 = heading3.svg diff --git a/nw/assets/icons/typicons_colour_light/heading0.svg b/nw/assets/icons/typicons_colour_light/heading0.svg new file mode 100644 index 00000000..87155b79 --- /dev/null +++ b/nw/assets/icons/typicons_colour_light/heading0.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/typicons_colour_light/icons.conf b/nw/assets/icons/typicons_colour_light/icons.conf index 4f9ffb6c..775aa4d5 100644 --- a/nw/assets/icons/typicons_colour_light/icons.conf +++ b/nw/assets/icons/typicons_colour_light/icons.conf @@ -29,6 +29,7 @@ cls_archive = delete.svg cls_trash = trash.svg proj_document = document-text.svg proj_folder = folder.svg +doc_h0 = heading0.svg doc_h1 = heading1.svg doc_h2 = heading2.svg doc_h3 = heading3.svg diff --git a/nw/assets/icons/typicons_grey_dark/heading0.svg b/nw/assets/icons/typicons_grey_dark/heading0.svg new file mode 100644 index 00000000..4cf4dcc3 --- /dev/null +++ b/nw/assets/icons/typicons_grey_dark/heading0.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/typicons_grey_dark/icons.conf b/nw/assets/icons/typicons_grey_dark/icons.conf index 7dae46e9..7dc56936 100644 --- a/nw/assets/icons/typicons_grey_dark/icons.conf +++ b/nw/assets/icons/typicons_grey_dark/icons.conf @@ -29,6 +29,7 @@ cls_archive = delete.svg cls_trash = trash.svg proj_document = document-text.svg proj_folder = folder.svg +doc_h0 = heading0.svg doc_h1 = heading1.svg doc_h2 = heading2.svg doc_h3 = heading3.svg diff --git a/nw/assets/icons/typicons_grey_light/heading0.svg b/nw/assets/icons/typicons_grey_light/heading0.svg new file mode 100644 index 00000000..b6377afe --- /dev/null +++ b/nw/assets/icons/typicons_grey_light/heading0.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/typicons_grey_light/icons.conf b/nw/assets/icons/typicons_grey_light/icons.conf index f21000cb..7c47ba7d 100644 --- a/nw/assets/icons/typicons_grey_light/icons.conf +++ b/nw/assets/icons/typicons_grey_light/icons.conf @@ -29,6 +29,7 @@ cls_archive = delete.svg cls_trash = trash.svg proj_document = document-text.svg proj_folder = folder.svg +doc_h0 = heading0.svg doc_h1 = heading1.svg doc_h2 = heading2.svg doc_h3 = heading3.svg diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 61e2fca7..8b14e4de 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -529,6 +529,7 @@ class GuiIcons: "status_time" : (None, None), "status_stats" : (None, None), "status_lines" : (None, None), + "doc_h0" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), From 14a0e6bd6e76675584a7eb4d4c287271a9d39714 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 Jan 2021 00:13:26 +0100 Subject: [PATCH 04/15] Allow indexing of untitled pages, and add a ToC function to the index class --- nw/core/index.py | 92 +++++++++++++++++-- .../coreIndex_LoadSave_tagsIndex.json | 13 ++- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index c311b1be..6c4ebf65 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -341,6 +341,11 @@ class NWIndex(): lastText = "\n".join(theLines[nTitle-1:]) self._indexWordCounts(tHandle, isNovel, lastText, nTitle) + # Index page with no titles and references + if nTitle == 0: + self._indexPage(tHandle, isNovel, itemLayout) + self._indexWordCounts(tHandle, isNovel, theText, nTitle) + # Update timestamps for index changes nowTime = round(time()) self._timeIndex = nowTime @@ -400,6 +405,29 @@ class NWIndex(): return True + def _indexPage(self, tHandle, isNovel, itemLayout): + """Index a page with no title. + """ + theData = { + "level" : "H0", + "title" : "Untitled Page", + "layout" : itemLayout.name, + "synopsis" : "", + "cCount" : 0, + "wCount" : 0, + "pCount" : 0, + "updated" : round(time()), + } + + if isNovel: + if tHandle in self._novelIndex: + self._novelIndex[tHandle]["T000000"] = theData + else: + if tHandle in self._noteIndex: + self._noteIndex[tHandle]["T000000"] = theData + + return + def _indexWordCounts(self, tHandle, isNovel, theText, nTitle): """Count text stats and save the counts to the index. """ @@ -551,15 +579,61 @@ class NWIndex(): files, but skipping all note files. """ for tItem in self.theProject.projTree: - if tItem is not None: - if not tItem.isExported and skipExcluded: - continue - tHandle = tItem.itemHandle - if tHandle not in self._novelIndex: - continue - for sTitle in sorted(self._novelIndex[tHandle]): - tKey = "%s:%s" % (tHandle, sTitle) - yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle] + if tItem is None: + continue + if not tItem.isExported and skipExcluded: + continue + + tHandle = tItem.itemHandle + if tHandle not in self._novelIndex: + continue + + for sTitle in sorted(self._novelIndex[tHandle]): + tKey = "%s:%s" % (tHandle, sTitle) + yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle] + + def getTableOfContents(self, maxDepth, skipExcluded=True): + """Generate a table of contents up to a maxiumum depth. + """ + hLevel = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} + + tOrder = [] + tData = {} + pKey = None + for tItem in self.theProject.projTree: + if tItem is None: + continue + if not tItem.isExported and skipExcluded: + continue + + tHandle = tItem.itemHandle + if tHandle not in self._novelIndex: + continue + + for sTitle in sorted(self._novelIndex[tHandle]): + tKey = "%s:%s" % (tHandle, sTitle) + theData = self._novelIndex[tHandle][sTitle] + iLevel = hLevel.get(theData["level"], 0) + if iLevel > maxDepth: + if pKey in tData: + theData["wCount"] + tData[pKey]["words"] += theData["wCount"] + else: + pKey = tKey + tOrder.append(tKey) + tData[tKey] = { + "level": theData["level"], + "title": theData["title"], + "words": theData["wCount"], + } + + theToC = [] + for tKey in tOrder: + theToC.append(( + tKey, tData[tKey]["level"], tData[tKey]["title"], tData[tKey]["words"] + )) + + return theToC def getCounts(self, tHandle, sTitle=None): """Returns the counts for a file, or a section of a file diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 281073cc..2aa3df3c 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -300,7 +300,18 @@ "updated": 123 } }, - "8c58a65414c23": {}, + "8c58a65414c23": { + "T000000": { + "level": "H0", + "title": "Untitled Page", + "layout": "PAGE", + "synopsis": "", + "cCount": 1058, + "wCount": 176, + "pCount": 2, + "updated": 123 + } + }, "88d59a277361b": { "T000001": { "level": "H2", From 28dd7e4ca71b9cf973c4baab54bb2e563f1d8af1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 Jan 2021 00:14:03 +0100 Subject: [PATCH 05/15] Add Chapters tab to Project Details tool --- nw/core/options.py | 5 ++ nw/gui/projdetails.py | 155 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index d35a26f5..f0a39d45 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -91,6 +91,11 @@ class OptionState(): "GuiProjectDetails": { "winWidth", "winHeight", + "widthCol0", + "widthCol1", + "widthCol2", + "widthCol3", + "widthCol4", }, } diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 57610284..6df69129 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -27,8 +27,12 @@ import nw import logging +import math -from PyQt5.QtWidgets import QDialogButtonBox +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtWidgets import ( + QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem +) from nw.gui.custom import PagedDialog @@ -59,9 +63,11 @@ class GuiProjectDetails(PagedDialog): self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH)) ) - # self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) + self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) + self.tabChapters = GuiProjectDetailsChapters(self.theParent, self.theProject) - # self.addTab(self.tabMain, "Settings") + self.addTab(self.tabMain, "Overview") + self.addTab(self.tabChapters, "Chapters") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) @@ -92,9 +98,152 @@ class GuiProjectDetails(PagedDialog): winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) + chColWidth = self.tabChapters.getColumnSizes() + widthCol0 = self.mainConf.rpxInt(chColWidth[0]) + widthCol1 = self.mainConf.rpxInt(chColWidth[1]) + widthCol2 = self.mainConf.rpxInt(chColWidth[2]) + widthCol3 = self.mainConf.rpxInt(chColWidth[3]) + widthCol4 = self.mainConf.rpxInt(chColWidth[4]) + self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) + self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0) + self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1) + self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2) + self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3) + self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4) return # END Class GuiProjectDetails + +class GuiProjectDetailsMain(QWidget): + + def __init__(self, theParent, theProject): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + + return + +# END Class GuiProjectDetailsMain + +class GuiProjectDetailsChapters(QWidget): + + C_TITLE = 0 + C_WORDS = 1 + C_PAGES = 2 + C_PAGE = 3 + C_PROG = 4 + + def __init__(self, theParent, theProject): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.theTheme = theParent.theTheme + self.theIndex = theParent.theIndex + self.optState = theProject.optState + + iPx = self.theTheme.baseIconSize + + self.chTree = QTreeWidget() + self.chTree.setIconSize(QSize(iPx, iPx)) + self.chTree.setIndentation(0) + self.chTree.setColumnCount(6) + self.chTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""]) + + treeHeadItem = self.chTree.headerItem() + 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.chTree.header() + treeHeader.setStretchLastSection(True) + treeHeader.setMinimumSectionSize(iPx + 6) + + # Get user's column width preferences + wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200)) + wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60)) + wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60)) + wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60)) + wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90)) + self.chTree.setColumnWidth(0, wCol0) + self.chTree.setColumnWidth(1, wCol1) + self.chTree.setColumnWidth(2, wCol2) + self.chTree.setColumnWidth(3, wCol3) + self.chTree.setColumnWidth(4, wCol4) + self.chTree.setColumnWidth(5, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.chTree) + + self.setLayout(self.outerBox) + + self._populateTree() + + return + + def getColumnSizes(self): + """Return the column widths for the tree columns. + """ + retVals = [ + self.chTree.columnWidth(0), + self.chTree.columnWidth(1), + self.chTree.columnWidth(2), + self.chTree.columnWidth(3), + self.chTree.columnWidth(4), + ] + return retVals + + ## + # Internal Functions + ## + + def _populateTree(self): + """Set the content of the chapter/page tree. + """ + self.chTree.clear() + + dblPages = True + wpPage = 100 + tPages = 1 + + theToC = self.theIndex.getTableOfContents(2) + theToC.append(("", "H0", "END", 0)) + + theList = [] + pTotal = 0 + for tKey, tLevel, tTitle, wCount in theToC: + pCount = math.ceil(wCount/wpPage) + if dblPages: + pCount += pCount%2 + pTotal += pCount + theList.append((tLevel, tTitle, wCount, pCount)) + + for tLevel, tTitle, wCount, pCount in theList: + newItem = QTreeWidgetItem() + + newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_%s" % tLevel.lower())) + 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, f"{tPages:n}") + newItem.setText(self.C_PROG, f"{100*(tPages-1)/pTotal:.2f}%") + + 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) + + tPages += pCount + + self.chTree.addTopLevelItem(newItem) + + return + +# END Class GuiProjectDetailsChapters From f96d78e9b8e703a1aa1ba2585a606e499431fac7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 Jan 2021 15:09:34 +0100 Subject: [PATCH 06/15] Contents tab now fully working --- nw/core/options.py | 2 + nw/gui/projdetails.py | 112 +++++++++++++++++++++++++++++++----------- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index f0a39d45..e409812d 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -96,6 +96,8 @@ class OptionState(): "widthCol2", "widthCol3", "widthCol4", + "wordsPerPage", + "clearDouble", }, } diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 6df69129..c4b48b24 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -31,10 +31,11 @@ import math from PyQt5.QtCore import Qt, QSize from PyQt5.QtWidgets import ( - QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem + QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem, + QLabel, QSpinBox, QGroupBox, QGridLayout ) -from nw.gui.custom import PagedDialog +from nw.gui.custom import PagedDialog, QSwitch logger = logging.getLogger(__name__) @@ -64,10 +65,10 @@ class GuiProjectDetails(PagedDialog): ) self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) - self.tabChapters = GuiProjectDetailsChapters(self.theParent, self.theProject) + self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject) self.addTab(self.tabMain, "Overview") - self.addTab(self.tabChapters, "Chapters") + self.addTab(self.tabContents, "Contents") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) @@ -98,20 +99,25 @@ class GuiProjectDetails(PagedDialog): winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) - chColWidth = self.tabChapters.getColumnSizes() - widthCol0 = self.mainConf.rpxInt(chColWidth[0]) - widthCol1 = self.mainConf.rpxInt(chColWidth[1]) - widthCol2 = self.mainConf.rpxInt(chColWidth[2]) - widthCol3 = self.mainConf.rpxInt(chColWidth[3]) - widthCol4 = self.mainConf.rpxInt(chColWidth[4]) + cColWidth = self.tabContents.getColumnSizes() + widthCol0 = self.mainConf.rpxInt(cColWidth[0]) + widthCol1 = self.mainConf.rpxInt(cColWidth[1]) + widthCol2 = self.mainConf.rpxInt(cColWidth[2]) + widthCol3 = self.mainConf.rpxInt(cColWidth[3]) + widthCol4 = self.mainConf.rpxInt(cColWidth[4]) - self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) - self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) - self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0) - self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1) - self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2) - self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3) - self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4) + wordsPerPage = self.tabContents.wpValue.value() + clearDouble = self.tabContents.dblValue.isChecked() + + self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) + self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) + self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0) + self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1) + self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2) + self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3) + self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4) + self.optState.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage) + self.optState.setValue("GuiProjectDetails", "clearDouble", clearDouble) return @@ -130,7 +136,7 @@ class GuiProjectDetailsMain(QWidget): # END Class GuiProjectDetailsMain -class GuiProjectDetailsChapters(QWidget): +class GuiProjectDetailsContents(QWidget): C_TITLE = 0 C_WORDS = 1 @@ -148,8 +154,14 @@ class GuiProjectDetailsChapters(QWidget): self.theIndex = theParent.theIndex self.optState = theProject.optState + # Internal + self._theToC = [] + iPx = self.theTheme.baseIconSize + # Contents Tree + # ============= + self.chTree = QTreeWidget() self.chTree.setIconSize(QSize(iPx, iPx)) self.chTree.setIndentation(0) @@ -166,7 +178,6 @@ class GuiProjectDetailsChapters(QWidget): treeHeader.setStretchLastSection(True) treeHeader.setMinimumSectionSize(iPx + 6) - # Get user's column width preferences wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200)) wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60)) wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60)) @@ -179,11 +190,45 @@ class GuiProjectDetailsChapters(QWidget): self.chTree.setColumnWidth(4, wCol4) self.chTree.setColumnWidth(5, 0) + # Options + # ======= + + wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 300) + clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True) + + self.optionsBox = QGroupBox("Options", self) + self.optionsForm = QGridLayout(self) + self.optionsBox.setLayout(self.optionsForm) + + self.wpLabel = QLabel("Words per page") + self.wpValue = QSpinBox() + self.wpValue.setMinimum(10) + self.wpValue.setMaximum(1000) + self.wpValue.setSingleStep(10) + self.wpValue.setValue(wordsPerPage) + self.wpValue.valueChanged.connect(self._populateTree) + + self.dblLabel = QLabel("Clear double pages") + self.dblValue = QSwitch(self, 2*iPx, iPx) + self.dblValue.setChecked(clearDouble) + self.dblValue.clicked.connect(self._populateTree) + + self.optionsForm.addWidget(self.wpLabel, 0, 0, 1, 1, Qt.AlignLeft) + self.optionsForm.addWidget(self.wpValue, 0, 1, 1, 1, Qt.AlignRight) + self.optionsForm.addWidget(self.dblLabel, 1, 0, 1, 1, Qt.AlignLeft) + self.optionsForm.addWidget(self.dblValue, 1, 1, 1, 1, Qt.AlignRight) + self.optionsForm.setColumnStretch(2, 1) + + # Assemble + # ======== + self.outerBox = QVBoxLayout() self.outerBox.addWidget(self.chTree) + self.outerBox.addWidget(self.optionsBox) self.setLayout(self.outerBox) + self._prepareData() self._populateTree() return @@ -204,27 +249,36 @@ class GuiProjectDetailsChapters(QWidget): # Internal Functions ## + def _prepareData(self): + """Extract the data for the tree. + """ + self._theToC = [] + self._theToC = self.theIndex.getTableOfContents(2) + self._theToC.append(("", "H0", "END", 0)) + return + + ## + # Slots + ## + def _populateTree(self): """Set the content of the chapter/page tree. """ - self.chTree.clear() + dblPages = self.dblValue.isChecked() + wpPage = self.wpValue.value() - dblPages = True - wpPage = 100 tPages = 1 - - theToC = self.theIndex.getTableOfContents(2) - theToC.append(("", "H0", "END", 0)) + pTotal = 0 theList = [] - pTotal = 0 - for tKey, tLevel, tTitle, wCount in theToC: + for tKey, tLevel, tTitle, wCount in self._theToC: pCount = math.ceil(wCount/wpPage) if dblPages: pCount += pCount%2 pTotal += pCount theList.append((tLevel, tTitle, wCount, pCount)) + self.chTree.clear() for tLevel, tTitle, wCount, pCount in theList: newItem = QTreeWidgetItem() @@ -233,7 +287,7 @@ class GuiProjectDetailsChapters(QWidget): newItem.setText(self.C_WORDS, f"{wCount:n}") newItem.setText(self.C_PAGES, f"{pCount:n}") newItem.setText(self.C_PAGE, f"{tPages:n}") - newItem.setText(self.C_PROG, f"{100*(tPages-1)/pTotal:.2f}%") + newItem.setText(self.C_PROG, f"{100*(tPages-1)/pTotal:.1f}\u202f%") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) @@ -246,4 +300,4 @@ class GuiProjectDetailsChapters(QWidget): return -# END Class GuiProjectDetailsChapters +# END Class GuiProjectDetailsContents From 8d218db35fef99d6ad6f265981a6857c7155d6de Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 Jan 2021 15:54:36 +0100 Subject: [PATCH 07/15] Project Details page Overview populated --- nw/core/index.py | 27 ++++++++++++++-- nw/gui/projdetails.py | 74 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 6c4ebf65..6128a177 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -42,6 +42,8 @@ logger = logging.getLogger(__name__) class NWIndex(): + H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} + def __init__(self, theProject, theParent): # Internal @@ -592,11 +594,30 @@ class NWIndex(): tKey = "%s:%s" % (tHandle, sTitle) yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle] + def getNovelCounts(self, skipExcluded=True): + """Count the number of titles in the novel project. + """ + hCount = [0, 0, 0, 0, 0] + for tItem in self.theProject.projTree: + if tItem is None: + continue + if not tItem.isExported and skipExcluded: + continue + + tHandle = tItem.itemHandle + if tHandle not in self._novelIndex: + continue + + for sTitle in self._novelIndex[tHandle]: + theData = self._novelIndex[tHandle][sTitle] + iLevel = self.H_LEVEL.get(theData["level"], 0) + hCount[iLevel] += 1 + + return hCount + def getTableOfContents(self, maxDepth, skipExcluded=True): """Generate a table of contents up to a maxiumum depth. """ - hLevel = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} - tOrder = [] tData = {} pKey = None @@ -613,7 +634,7 @@ class NWIndex(): for sTitle in sorted(self._novelIndex[tHandle]): tKey = "%s:%s" % (tHandle, sTitle) theData = self._novelIndex[tHandle][sTitle] - iLevel = hLevel.get(theData["level"], 0) + iLevel = self.H_LEVEL.get(theData["level"], 0) if iLevel > maxDepth: if pKey in tData: theData["wCount"] diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index c4b48b24..98bcb73e 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -30,9 +30,10 @@ import logging import math from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem, - QLabel, QSpinBox, QGroupBox, QGridLayout + QLabel, QSpinBox, QGroupBox, QGridLayout, QHBoxLayout ) from nw.gui.custom import PagedDialog, QSwitch @@ -131,6 +132,77 @@ class GuiProjectDetailsMain(QWidget): self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject + self.theTheme = theParent.theTheme + self.theIndex = theParent.theIndex + + fPx = self.theTheme.fontPixelSize + vPx = self.mainConf.pxInt(4) + hPx = self.mainConf.pxInt(12) + + # Header + # ====== + + self.bookTitle = QLabel(self.theProject.bookTitle) + bookFont = self.bookTitle.font() + bookFont.setPixelSize(round(2.2*fPx)) + bookFont.setWeight(QFont.Bold) + self.bookTitle.setFont(bookFont) + self.bookTitle.setAlignment(Qt.AlignHCenter) + + self.projName = QLabel("Working Title: %s" % self.theProject.projName) + workFont = self.projName.font() + workFont.setPixelSize(round(0.8*fPx)) + workFont.setItalic(True) + self.projName.setFont(workFont) + self.projName.setAlignment(Qt.AlignHCenter) + + self.bookAuthors = QLabel("By: %s" % ", ".join(self.theProject.bookAuthors)) + authFont = self.bookAuthors.font() + authFont.setPixelSize(round(1.2*fPx)) + self.bookAuthors.setFont(authFont) + self.bookAuthors.setAlignment(Qt.AlignHCenter) + + # Stats + # ===== + + hCounts = self.theIndex.getNovelCounts() + + self.wordCountLbl = QLabel("Words:") + self.wordCountVal = QLabel(f"{self.theProject.currWCount:n}") + + self.chapCountLbl = QLabel("Chapters:") + self.chapCountVal = QLabel(f"{hCounts[2]:n}") + + self.sceneCountLbl = QLabel("Scenes:") + self.sceneCountVal = QLabel(f"{hCounts[3]:n}") + + self.statsGrid = QGridLayout() + self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignLeft) + self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignRight) + self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignLeft) + self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignRight) + self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignLeft) + self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignRight) + self.statsGrid.setHorizontalSpacing(hPx) + self.statsGrid.setVerticalSpacing(vPx) + + self.statsBox = QHBoxLayout() + self.statsBox.addStretch(1) + self.statsBox.addLayout(self.statsGrid) + self.statsBox.addStretch(1) + + # Assemble + # ======== + + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.bookTitle) + self.outerBox.addWidget(self.projName) + self.outerBox.addWidget(self.bookAuthors) + self.outerBox.addSpacing(round(2.5*fPx)) + self.outerBox.addLayout(self.statsBox) + self.outerBox.addStretch(1) + + self.setLayout(self.outerBox) return From b5e11f23032557350f52281c7085c10a9ba825dc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 11 Jan 2021 19:47:42 +0100 Subject: [PATCH 08/15] Added more info to the forst tab of the Project Details dialog --- nw/core/project.py | 21 ++++++++++ nw/gui/mainmenu.py | 2 +- nw/gui/projdetails.py | 82 +++++++++++++++++++++++++++----------- tests/test_core_project.py | 21 ++++++++++ 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index ac7bf9ef..8cd9b2ec 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1112,11 +1112,32 @@ class NWProject(): # Getters ## + def getAuthors(self): + """Returns a formatted string of authors. + """ + nAuth = len(self.bookAuthors) + authString = "" + + if nAuth == 1: + authString = self.bookAuthors[0] + elif nAuth > 1: + authString = "%s and %s" % ( + ", ".join(self.bookAuthors[0:-1]), self.bookAuthors[-1] + ) + + return authString + def getSessionWordCount(self): """Returns the number of words added or removed this session. """ return self.currWCount - self.lastWCount + def getCurrentEditTime(self): + """Get the total project edit time, including the time spent in + the current session. + """ + return round(self.editTime + time() - self.projOpened) + def getProjectItems(self): """This function ensures that the item tree loaded is sent to the GUI tree view in such a way that the tree can be built. That diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 44348af6..77677e21 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -222,7 +222,7 @@ class GuiMainMenu(QMenuBar): # Project > Project Details self.aProjectDetails = QAction("Project Details", self) self.aProjectDetails.setStatusTip("Project details") - self.aProjectDetails.setShortcut("Ctrl+Shift+E") + self.aProjectDetails.setShortcut("Shift+F6") self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) self.projMenu.addAction(self.aProjectDetails) diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 98bcb73e..0270476d 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem, - QLabel, QSpinBox, QGroupBox, QGridLayout, QHBoxLayout + QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit ) from nw.gui.custom import PagedDialog, QSwitch @@ -148,6 +148,7 @@ class GuiProjectDetailsMain(QWidget): bookFont.setWeight(QFont.Bold) self.bookTitle.setFont(bookFont) self.bookTitle.setAlignment(Qt.AlignHCenter) + self.bookTitle.setWordWrap(True) self.projName = QLabel("Working Title: %s" % self.theProject.projName) workFont = self.projName.font() @@ -155,12 +156,14 @@ class GuiProjectDetailsMain(QWidget): workFont.setItalic(True) self.projName.setFont(workFont) self.projName.setAlignment(Qt.AlignHCenter) + self.projName.setWordWrap(True) - self.bookAuthors = QLabel("By: %s" % ", ".join(self.theProject.bookAuthors)) + self.bookAuthors = QLabel("By %s" % self.theProject.getAuthors()) authFont = self.bookAuthors.font() authFont.setPixelSize(round(1.2*fPx)) self.bookAuthors.setFont(authFont) self.bookAuthors.setAlignment(Qt.AlignHCenter) + self.bookAuthors.setWordWrap(True) # Stats # ===== @@ -176,20 +179,39 @@ class GuiProjectDetailsMain(QWidget): self.sceneCountLbl = QLabel("Scenes:") self.sceneCountVal = QLabel(f"{hCounts[3]:n}") + self.revCountLbl = QLabel("Revisions:") + self.revCountVal = QLabel(f"{self.theProject.saveCount:n}") + + edTime = self.theProject.getCurrentEditTime() + self.editTimeLbl = QLabel("Editing Time:") + self.editTimeVal = QLabel(f"{edTime//3600:02d}:{edTime%3600//60:02d}") + self.statsGrid = QGridLayout() - self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignRight) + 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) - self.statsBox = QHBoxLayout() - self.statsBox.addStretch(1) - self.statsBox.addLayout(self.statsGrid) - self.statsBox.addStretch(1) + # Meta + # ==== + + self.projPathLbl = QLabel("Path:") + self.projPathVal = QLineEdit() + self.projPathVal.setText(self.theProject.projPath) + self.projPathVal.setReadOnly(True) + + self.projPathBox = QHBoxLayout() + self.projPathBox.addWidget(self.projPathLbl) + self.projPathBox.addWidget(self.projPathVal) + self.projPathBox.setSpacing(hPx) # Assemble # ======== @@ -199,8 +221,10 @@ class GuiProjectDetailsMain(QWidget): self.outerBox.addWidget(self.projName) self.outerBox.addWidget(self.bookAuthors) self.outerBox.addSpacing(round(2.5*fPx)) - self.outerBox.addLayout(self.statsBox) + self.outerBox.addLayout(self.statsGrid) + self.outerBox.addSpacing(round(0.8*fPx)) self.outerBox.addStretch(1) + self.outerBox.addLayout(self.projPathBox) self.setLayout(self.outerBox) @@ -230,6 +254,7 @@ class GuiProjectDetailsContents(QWidget): self._theToC = [] iPx = self.theTheme.baseIconSize + hPx = self.mainConf.pxInt(12) # Contents Tree # ============= @@ -265,38 +290,49 @@ class GuiProjectDetailsContents(QWidget): # Options # ======= - wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 300) + wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350) clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True) - self.optionsBox = QGroupBox("Options", self) - self.optionsForm = QGridLayout(self) - self.optionsBox.setLayout(self.optionsForm) + wordsHelp = ( + "Typical word count for a 5\u00d78 inch book page with 11 pt font is 350." + ) + dblHelp = ( + "Assume a new chapter or partition always start on an odd numbered page." + ) self.wpLabel = QLabel("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.dblLabel = QLabel("Clear double pages") + self.dblLabel.setToolTip(dblHelp) + self.dblValue = QSwitch(self, 2*iPx, iPx) self.dblValue.setChecked(clearDouble) + self.dblValue.setToolTip(dblHelp) self.dblValue.clicked.connect(self._populateTree) - self.optionsForm.addWidget(self.wpLabel, 0, 0, 1, 1, Qt.AlignLeft) - self.optionsForm.addWidget(self.wpValue, 0, 1, 1, 1, Qt.AlignRight) - self.optionsForm.addWidget(self.dblLabel, 1, 0, 1, 1, Qt.AlignLeft) - self.optionsForm.addWidget(self.dblValue, 1, 1, 1, 1, Qt.AlignRight) - self.optionsForm.setColumnStretch(2, 1) + self.optionsBox = QHBoxLayout() + self.optionsBox.addWidget(self.wpLabel) + self.optionsBox.addWidget(self.wpValue) + self.optionsBox.addStretch(1) + self.optionsBox.addWidget(self.dblLabel) + self.optionsBox.addWidget(self.dblValue) + self.optionsBox.setSpacing(hPx) # Assemble # ======== self.outerBox = QVBoxLayout() self.outerBox.addWidget(self.chTree) - self.outerBox.addWidget(self.optionsBox) + self.outerBox.addLayout(self.optionsBox) self.setLayout(self.outerBox) diff --git a/tests/test_core_project.py b/tests/test_core_project.py index 57ee8f51..6fbe14c9 100644 --- a/tests/test_core_project.py +++ b/tests/test_core_project.py @@ -682,10 +682,31 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir): assert theProject.bookTitle == "A Title" # Project Authors + # Check that the list is cleaned up and that it can be extracted as + # a properly formatted string, depending on number of names assert not theProject.setBookAuthors([]) assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ") assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + assert theProject.setBookAuthors("") + assert theProject.getAuthors() == "" + + assert theProject.setBookAuthors("Jane Doe") + assert theProject.getAuthors() == "Jane Doe" + + assert theProject.setBookAuthors("Jane Doe\nJohn Doh") + assert theProject.getAuthors() == "Jane Doe and John Doh" + + assert theProject.setBookAuthors("Jane Doe\nJohn Doh\nBod Owens") + assert theProject.getAuthors() == "Jane Doe, John Doh and Bod Owens" + + # Edit Time + theProject.editTime = 1234 + theProject.projOpened = 1600000000 + monkeypatch.setattr("nw.core.project.time", lambda: 1600005600) + assert theProject.getCurrentEditTime() == 6834 + monkeypatch.undo() + # Trash folder # Should create on first call, and just returned on later calls assert theProject.projTree["73475cb40a568"] is None From 981e26e1b6c728b08c04dc87fe08644d9a4dd4b8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 11 Jan 2021 21:02:45 +0100 Subject: [PATCH 09/15] Use actual novel word count in project details, and update index test --- nw/core/index.py | 65 +++++++++++++++++---------------- nw/gui/projdetails.py | 5 +-- tests/test_core_index.py | 78 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 34 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 6128a177..2b14a5d4 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -292,10 +292,12 @@ class NWIndex(): "updated" : round(time()), } if itemLayout == nwItemLayout.NOTE: + self._novelIndex.pop(tHandle, None) self._noteIndex[tHandle] = {} isNovel = False else: self._novelIndex[tHandle] = {} + self._noteIndex.pop(tHandle, None) isNovel = True # Also clear references to file in tag index @@ -580,34 +582,26 @@ class NWIndex(): they appear in the tree view and in the respective document files, but skipping all note files. """ - for tItem in self.theProject.projTree: - if tItem is None: - continue - if not tItem.isExported and skipExcluded: - continue - - tHandle = tItem.itemHandle - if tHandle not in self._novelIndex: - continue - + for tHandle in self._listNovelHandles(skipExcluded): for sTitle in sorted(self._novelIndex[tHandle]): tKey = "%s:%s" % (tHandle, sTitle) yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle] - def getNovelCounts(self, skipExcluded=True): + def getNovelWordCount(self, skipExcluded=True): + """Count the number of words in the novel project. + """ + wCount = 0 + for tHandle in self._listNovelHandles(skipExcluded): + for sTitle in self._novelIndex[tHandle]: + wCount += self._novelIndex[tHandle][sTitle]["wCount"] + + return wCount + + def getNovelTitleCounts(self, skipExcluded=True): """Count the number of titles in the novel project. """ hCount = [0, 0, 0, 0, 0] - for tItem in self.theProject.projTree: - if tItem is None: - continue - if not tItem.isExported and skipExcluded: - continue - - tHandle = tItem.itemHandle - if tHandle not in self._novelIndex: - continue - + for tHandle in self._listNovelHandles(skipExcluded): for sTitle in self._novelIndex[tHandle]: theData = self._novelIndex[tHandle][sTitle] iLevel = self.H_LEVEL.get(theData["level"], 0) @@ -621,16 +615,7 @@ class NWIndex(): tOrder = [] tData = {} pKey = None - for tItem in self.theProject.projTree: - if tItem is None: - continue - if not tItem.isExported and skipExcluded: - continue - - tHandle = tItem.itemHandle - if tHandle not in self._novelIndex: - continue - + for tHandle in self._listNovelHandles(skipExcluded): for sTitle in sorted(self._novelIndex[tHandle]): tKey = "%s:%s" % (tHandle, sTitle) theData = self._novelIndex[tHandle][sTitle] @@ -740,4 +725,22 @@ class NWIndex(): return theRef[1], theRef[0], theRef[3] return None, 0, "T000000" + ## + # Internal Functions + ## + + def _listNovelHandles(self, skipExcluded): + """Return a list of all handles that exist in the novel index. + """ + theHandles = [] + for tItem in self.theProject.projTree: + if tItem is None: + continue + if not tItem.isExported and skipExcluded: + continue + if tItem.itemHandle in self._novelIndex: + theHandles.append(tItem.itemHandle) + + return theHandles + # END Class NWIndex diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 0270476d..ae001483 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -168,10 +168,11 @@ class GuiProjectDetailsMain(QWidget): # Stats # ===== - hCounts = self.theIndex.getNovelCounts() + hCounts = self.theIndex.getNovelTitleCounts() + nwCount = self.theIndex.getNovelWordCount() self.wordCountLbl = QLabel("Words:") - self.wordCountVal = QLabel(f"{self.theProject.currWCount:n}") + self.wordCountVal = QLabel(f"{nwCount:n}") self.chapCountLbl = QLabel("Chapters:") self.chapCountVal = QLabel(f"{hCounts[2]:n}") diff --git a/tests/test_core_index.py b/tests/test_core_index.py index b19af00d..d865cd41 100644 --- a/tests/test_core_index.py +++ b/tests/test_core_index.py @@ -276,6 +276,7 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI): assert not theIndex.scanText(xHandle, "Hello World!") # Make some usable items + pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c") nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c") @@ -310,7 +311,7 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI): "#### Title Four\n\n" "% synopsis: Synopsis Four.\n\n" "Paragraph Four.\n\n" - "##### Title Five\n\n" # Not interpreted as a title, the hashes is counted as a word + "##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word "Paragraph Five.\n\n" )) assert theIndex._refIndex[nHandle].get("T000000", None) is not None # Always there @@ -411,6 +412,33 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI): [[3, "@pov", "One"], [5, "@char", "Two"]] ) + # Page wo/Title + theProject.projTree[pHandle].itemLayout = nwItemLayout.PAGE + assert theIndex.scanText(pHandle, ( + "This is a page with some text on it.\n\n" + )) + assert theIndex._novelIndex[pHandle]["T000000"]["level"] == "H0" + assert theIndex._novelIndex[pHandle]["T000000"]["title"] == "Untitled Page" + assert theIndex._novelIndex[pHandle]["T000000"]["layout"] == "PAGE" + assert theIndex._novelIndex[pHandle]["T000000"]["synopsis"] == "" + assert theIndex._novelIndex[pHandle]["T000000"]["cCount"] == 36 + assert theIndex._novelIndex[pHandle]["T000000"]["wCount"] == 9 + assert theIndex._novelIndex[pHandle]["T000000"]["pCount"] == 1 + assert pHandle not in theIndex._noteIndex + + theProject.projTree[pHandle].itemLayout = nwItemLayout.NOTE + assert theIndex.scanText(pHandle, ( + "This is a page with some text on it.\n\n" + )) + assert theIndex._noteIndex[pHandle]["T000000"]["level"] == "H0" + assert theIndex._noteIndex[pHandle]["T000000"]["title"] == "Untitled Page" + assert theIndex._noteIndex[pHandle]["T000000"]["layout"] == "NOTE" + assert theIndex._noteIndex[pHandle]["T000000"]["synopsis"] == "" + assert theIndex._noteIndex[pHandle]["T000000"]["cCount"] == 36 + assert theIndex._noteIndex[pHandle]["T000000"]["wCount"] == 9 + assert theIndex._noteIndex[pHandle]["T000000"]["pCount"] == 1 + assert pHandle not in theIndex._novelIndex + assert theProject.closeProject() # END Test testCoreIndex_ScanText @@ -579,6 +607,54 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI): assert wC == 12 assert pC == 2 + ## + # Novel Stats + ## + + hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c") + sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c") + tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c") + + theProject.projTree[hHandle].itemLayout == nwItemLayout.CHAPTER + theProject.projTree[sHandle].itemLayout == nwItemLayout.SCENE + theProject.projTree[tHandle].itemLayout == nwItemLayout.SCENE + + assert theIndex.scanText(hHandle, "## Chapter One\n\n") + assert theIndex.scanText(sHandle, "### Scene One\n\n") + assert theIndex.scanText(tHandle, "### Scene Two\n\n") + + assert theIndex._listNovelHandles(False) == [nHandle, hHandle, sHandle, tHandle] + assert theIndex._listNovelHandles(True) == [hHandle, sHandle, tHandle] + + # Add a fake handle to the tree and check that it's ignored + theProject.projTree._treeOrder.append("0000000000000") + assert theIndex._listNovelHandles(False) == [nHandle, hHandle, sHandle, tHandle] + theProject.projTree._treeOrder.remove("0000000000000") + + # Extract stats + assert theIndex.getNovelWordCount(False) == 30 + assert theIndex.getNovelWordCount(True) == 6 + assert theIndex.getNovelTitleCounts(False) == [0, 2, 1, 2, 0] + assert theIndex.getNovelTitleCounts(True) == [0, 0, 1, 2, 0] + + # Table of Contents + assert theIndex.getTableOfContents(0, True) == [] + assert theIndex.getTableOfContents(1, True) == [] + assert theIndex.getTableOfContents(2, True) == [ + ("%s:T000001" % hHandle, "H2", "Chapter One", 6), + ] + assert theIndex.getTableOfContents(3, True) == [ + ("%s:T000001" % hHandle, "H2", "Chapter One", 2), + ("%s:T000001" % sHandle, "H3", "Scene One", 2), + ("%s:T000001" % tHandle, "H3", "Scene Two", 2), + ] + + assert theIndex.getTableOfContents(0, False) == [] + assert theIndex.getTableOfContents(1, False) == [ + ("%s:T000001" % nHandle, "H1", "Hello World!", 12), + ("%s:T000011" % nHandle, "H1", "Hello World!", 18), + ] + assert theProject.closeProject() # END Test testCoreIndex_ExtractData From 5f17b28e05869c0217c6218f524fd7a05a61b07e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 13 Jan 2021 23:51:24 +0100 Subject: [PATCH 10/15] Remove old details panel in Project Settings --- nw/gui/projsettings.py | 101 ++--------------------------------------- 1 file changed, 3 insertions(+), 98 deletions(-) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 52df0589..23426cb5 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -31,9 +31,9 @@ import logging from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush from PyQt5.QtWidgets import ( - QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel, - QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, - QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox + QHBoxLayout, QVBoxLayout, QLineEdit, QPlainTextEdit, QLabel, QWidget, + QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, QColorDialog, + QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox ) from nw.constants import nwAlert @@ -68,13 +68,11 @@ class GuiProjectSettings(PagedDialog): ) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) - self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject) self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject, True) self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False) self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject) self.addTab(self.tabMain, "Settings") - self.addTab(self.tabMeta, "Details") self.addTab(self.tabStatus, "Status") self.addTab(self.tabImport, "Importance") self.addTab(self.tabReplace, "Auto-Replace") @@ -238,94 +236,6 @@ class GuiProjectEditMain(QWidget): # END Class GuiProjectEditMain -class GuiProjectEditMeta(QWidget): - - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) - - self.mainConf = nw.CONFIG - self.theParent = theParent - self.theProject = theProject - - xInd = self.mainConf.pxInt(8) - - # The Form - self.mainForm = QGridLayout() - self.setLayout(self.mainForm) - - self.headLabel = QLabel("Project Details") - - self.nameLabel = QLabel("Working title:") - self.nameLabel.setIndent(xInd) - self.nameValue = QLabel(self.theProject.projName) - self.nameValue.setWordWrap(True) - - self.pathLabel = QLabel("Project path:") - self.pathLabel.setIndent(xInd) - self.pathValue = QLabel(self.theProject.projPath) - self.pathValue.setWordWrap(True) - self.pathValue.setTextInteractionFlags(Qt.TextSelectableByMouse) - self.pathValue.setCursor(Qt.IBeamCursor) - - self.revLabel = QLabel("Revision count:") - self.revLabel.setIndent(xInd) - self.revValue = QLabel(f"{self.theProject.saveCount:n}") - - editHours = self.theProject.editTime/3600 - self.editLabel = QLabel("Edit time:") - self.editLabel.setIndent(xInd) - self.editValue = QLabel(f"{editHours:.2f} hours") - - self.statsLabel = QLabel("Project Stats") - - nR, nD, nF = self.theProject.projTree.countTypes() - - self.nRootLabel = QLabel("Root folders:") - self.nRootLabel.setIndent(xInd) - self.nRootValue = QLabel(f"{nR:n}") - - self.nDirLabel = QLabel("Folders:") - self.nDirLabel.setIndent(xInd) - self.nDirValue = QLabel(f"{nD:n}") - - self.nFileLabel = QLabel("Documents:") - self.nFileLabel.setIndent(xInd) - self.nFileValue = QLabel(f"{nF:n}") - - self.wordsLabel = QLabel("Word count:") - self.wordsLabel.setIndent(xInd) - self.wordsValue = QLabel(f"{self.theProject.currWCount:n}") - - self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop) - self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nameValue, 1, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.pathLabel, 2, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.pathValue, 2, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.revLabel, 3, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.revValue, 3, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.editLabel, 4, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.editValue, 4, 1, 1, 1, Qt.AlignTop) - - self.mainForm.addWidget(self.statsLabel, 5, 0, 1, 2, Qt.AlignTop) - self.mainForm.addWidget(self.nRootLabel, 6, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nRootValue, 6, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nDirLabel, 7, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nDirValue, 7, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nFileLabel, 8, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.nFileValue, 8, 1, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.wordsLabel, 9, 0, 1, 1, Qt.AlignTop) - self.mainForm.addWidget(self.wordsValue, 9, 1, 1, 1, Qt.AlignTop) - - self.mainForm.setVerticalSpacing(self.mainConf.pxInt(6)) - self.mainForm.setHorizontalSpacing(self.mainConf.pxInt(12)) - self.mainForm.setColumnStretch(0, 0) - self.mainForm.setColumnStretch(1, 1) - self.mainForm.setRowStretch(10, 1) - - return - -# END Class GuiProjectEditMeta - class GuiProjectEditStatus(QWidget): def __init__(self, theParent, theProject, isStatus): @@ -414,7 +324,6 @@ class GuiProjectEditStatus(QWidget): def _selectColour(self): """Open a dialog to select the status icon colour. """ - logger.verbose("Item colour button clicked") if self.selColour is not None: newCol = QColorDialog.getColor( self.selColour, self, "Select Colour", QColorDialog.DontUseNativeDialog @@ -430,7 +339,6 @@ class GuiProjectEditStatus(QWidget): def _newItem(self): """Create a new status item. """ - logger.verbose("New item button clicked") newItem = self._addItem("New Item", (0, 0, 0), None, 0) newItem.setBackground(QBrush(QColor(0, 255, 0, 80))) self.colChanged = True @@ -439,7 +347,6 @@ class GuiProjectEditStatus(QWidget): def _delItem(self): """Delete a status item. """ - logger.verbose("Delete item button clicked") selItem = self._getSelectedItem() if selItem is not None: iRow = self.listBox.row(selItem) @@ -456,7 +363,6 @@ class GuiProjectEditStatus(QWidget): def _saveItem(self): """Save changes made to a status item. """ - logger.verbose("Save item button clicked") selItem = self._getSelectedItem() if selItem is not None: selIdx = selItem.data(Qt.UserRole) @@ -491,7 +397,6 @@ class GuiProjectEditStatus(QWidget): """Extract the info of a selected item and populate the settings boxes and button. """ - logger.verbose("Item selected") selItem = self._getSelectedItem() if selItem is not None: selIdx = selItem.data(Qt.UserRole) From f6b7a35c811b417842924708e329ee11832a808f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 13 Jan 2021 23:52:09 +0100 Subject: [PATCH 11/15] Add some more unicode constants and rename thin non-breaking space variable --- nw/common.py | 6 +++--- nw/constants/constants.py | 10 +++++++--- nw/core/tohtml.py | 2 +- nw/gui/doceditor.py | 2 +- tests/test_gui_mainmenu.py | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/nw/common.py b/nw/common.py index f0e5900e..922c7b6f 100644 --- a/nw/common.py +++ b/nw/common.py @@ -152,11 +152,11 @@ def formatInt(theInt): theVal /= 1000.0 if theVal < 1000.0: if theVal < 10.0: - return f"{theVal:4.2f}{nwUnicode.U_THNSP}{pF}" + return f"{theVal:4.2f}{nwUnicode.U_THSP}{pF}" elif theVal < 100.0: - return f"{theVal:4.1f}{nwUnicode.U_THNSP}{pF}" + return f"{theVal:4.1f}{nwUnicode.U_THSP}{pF}" else: - return f"{theVal:3.0f}{nwUnicode.U_THNSP}{pF}" + return f"{theVal:3.0f}{nwUnicode.U_THSP}{pF}" return str(theInt) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index a39bb8e1..8efbec6f 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -241,7 +241,7 @@ class nwQuotes(): # END Class nwQuotes class nwUnicode: - """Supported unicode character constants and translation maps for HTML. + """Supported unicode character constants and their HTML equivalents. """ # Unicode Constants @@ -276,8 +276,10 @@ class nwUnicode: ## Spaces and Lines U_NBSP = "\u00a0" # Non-breaking space - U_THNSP = "\u2009" # Thin space + U_THSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + U_ENSP = "\u2002" # Short (en) space + U_EMSP = "\u2003" # Long (em) space U_LSEP = "\u2028" # Line separator U_PSEP = "\u2029" # Paragraph separator @@ -328,8 +330,10 @@ class nwUnicode: ## Spaces H_NBSP = " " - H_THNSP = " " + H_THSP = " " H_THNBSP = " " + H_ENSP = " " + H_EMSP = " " ## Symbols H_CHECK = "✔" diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 55da29d1..41807586 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -53,7 +53,7 @@ class ToHtml(Tokenizer): nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, nwUnicode.U_NBSP : nwUnicode.H_NBSP, - nwUnicode.U_THNSP : nwUnicode.H_THNSP, + nwUnicode.U_THSP : nwUnicode.H_THSP, nwUnicode.U_THNBSP : nwUnicode.H_THNBSP, nwUnicode.U_MAPOSS : nwUnicode.H_RSQUO, } diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 3421276c..e155c9fd 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -734,7 +734,7 @@ class GuiDocEditor(QTextEdit): elif theInsert == nwDocInsert.NB_SPACE: theText = nwUnicode.U_NBSP elif theInsert == nwDocInsert.THIN_SPACE: - theText = nwUnicode.U_THNSP + theText = nwUnicode.U_THSP elif theInsert == nwDocInsert.THIN_NB_SPACE: theText = nwUnicode.U_THNBSP elif theInsert == nwDocInsert.SHORT_DASH: diff --git a/tests/test_gui_mainmenu.py b/tests/test_gui_mainmenu.py index 33408505..86e3b749 100644 --- a/tests/test_gui_mainmenu.py +++ b/tests/test_gui_mainmenu.py @@ -410,7 +410,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj): nwGUI.docEditor.clear() nwGUI.mainMenu.aInsThinSpace.activate(QAction.Trigger) - assert nwGUI.docEditor.getText() == nwUnicode.U_THNSP + assert nwGUI.docEditor.getText() == nwUnicode.U_THSP nwGUI.docEditor.clear() nwGUI.mainMenu.aInsThinNBSpace.activate(QAction.Trigger) From 3bc80060283ca6077072522aaaacd998829442e2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 13 Jan 2021 23:52:48 +0100 Subject: [PATCH 12/15] Some minor improvements to the Project Detals dialog --- nw/gui/projdetails.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index ae001483..32131002 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -37,6 +37,7 @@ from PyQt5.QtWidgets import ( ) from nw.gui.custom import PagedDialog, QSwitch +from nw.constants import nwUnicode logger = logging.getLogger(__name__) @@ -274,19 +275,20 @@ class GuiProjectDetailsContents(QWidget): treeHeader = self.chTree.header() treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setMinimumSectionSize(hPx) wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200)) wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60)) wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60)) wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60)) wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90)) + self.chTree.setColumnWidth(0, wCol0) self.chTree.setColumnWidth(1, wCol1) self.chTree.setColumnWidth(2, wCol2) self.chTree.setColumnWidth(3, wCol3) self.chTree.setColumnWidth(4, wCol4) - self.chTree.setColumnWidth(5, 0) + self.chTree.setColumnWidth(5, hPx) # Options # ======= @@ -295,7 +297,7 @@ class GuiProjectDetailsContents(QWidget): clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True) wordsHelp = ( - "Typical word count for a 5\u00d78 inch book page with 11 pt font is 350." + "Typical word count for a 5 by 8 inch book page with 11 pt font is 350." ) dblHelp = ( "Assume a new chapter or partition always start on an odd numbered page." @@ -380,7 +382,7 @@ class GuiProjectDetailsContents(QWidget): pTotal = 0 theList = [] - for tKey, tLevel, tTitle, wCount in self._theToC: + for _, tLevel, tTitle, wCount in self._theToC: pCount = math.ceil(wCount/wpPage) if dblPages: pCount += pCount%2 @@ -391,12 +393,15 @@ class GuiProjectDetailsContents(QWidget): for tLevel, tTitle, wCount, pCount in theList: newItem = QTreeWidgetItem() + if tLevel == "H2": + tTitle = nwUnicode.U_ENSP+tTitle + newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_%s" % tLevel.lower())) 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, f"{tPages:n}") - newItem.setText(self.C_PROG, f"{100*(tPages-1)/pTotal:.1f}\u202f%") + newItem.setText(self.C_PROG, f"{100*(tPages - 1)/pTotal:.1f}{nwUnicode.U_THSP}%") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) From 0bfc1668996c3d3d3b51a40c6851bde3c5db1742 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 14 Jan 2021 18:31:27 +0100 Subject: [PATCH 13/15] Added a few buttons below the project tree now that the tabs make room for it --- nw/assets/icons/fallback/settings-dark.svg | 31 ++++++++++++++++++ nw/assets/icons/fallback/settings.svg | 31 ++++++++++++++++++ nw/gui/itemdetails.py | 28 +++++++++------- nw/gui/theme.py | 19 +++++++---- nw/guimain.py | 37 ++++++++++++++++++++-- 5 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 nw/assets/icons/fallback/settings-dark.svg create mode 100644 nw/assets/icons/fallback/settings.svg diff --git a/nw/assets/icons/fallback/settings-dark.svg b/nw/assets/icons/fallback/settings-dark.svg new file mode 100644 index 00000000..e2014bd8 --- /dev/null +++ b/nw/assets/icons/fallback/settings-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/settings.svg b/nw/assets/icons/fallback/settings.svg new file mode 100644 index 00000000..edc89a8f --- /dev/null +++ b/nw/assets/icons/fallback/settings.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index cbb006df..1a48e9cb 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -190,6 +190,22 @@ class GuiItemDetails(QWidget): # Class Methods ## + def clearDetails(self): + """Clear all the data values. + """ + self.labelFlag.setPixmap(QPixmap(1, 1)) + self.labelData.setText("") + self.statusFlag.setPixmap(QPixmap(1, 1)) + self.statusData.setText("") + self.classFlag.setText("") + self.classData.setText("") + self.layoutFlag.setText("") + self.layoutData.setText("") + self.cCountData.setText("–") + self.wCountData.setText("–") + self.pCountData.setText("–") + return + def updateCounts(self, tHandle, cC, wC, pC): """Just update the counts if the handle is the same as the one we're already showing. @@ -207,17 +223,7 @@ class GuiItemDetails(QWidget): nwItem = self.theProject.projTree[tHandle] if nwItem is None: - self.labelFlag.setText("") - self.labelData.setText("") - self.statusFlag.setText("") - self.statusData.setText("") - self.classFlag.setText("") - self.classData.setText("") - self.layoutFlag.setText("") - self.layoutData.setText("") - self.cCountData.setText("–") - self.wCountData.setText("–") - self.pCountData.setText("–") + self.clearDetails() else: theLabel = nwItem.itemName diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 8b14e4de..ef97c493 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -473,6 +473,9 @@ class GuiTheme: return def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault): + """Simple wrapper for the config parser to check that the entry + exists before attempting to load it. + """ if confParser.has_section(cnfSec): if confParser.has_option(cnfSec, cnfName): return confParser.get(cnfSec, cnfName) @@ -500,12 +503,13 @@ class GuiIcons: the ICON_MAP data tuple[0]. This will let Qt pull the closest system icon. * Third action is to look up the freedesktop icon theme name using - the fromTheme Qt call. This generally produces the same results + the fromTheme Qt call. This generally produces the same result as the step above, but has more icons available in other cases. * Fourth, and finally, the icon is looked up in the fallback folder. Files in this folder must have the same file name as the - novelWriter internal icon key, with '-dark' appended to it for - the dark background version of the icon. + novelWriter internal icon key, with '-dark' appended to them for + the dark background version of the icon. If no dark icon exists, + the non-dark version will be returned. """ ICON_MAP = { @@ -563,6 +567,7 @@ class GuiIcons: "reference" : (None, None), "backward" : (None, None), "forward" : (None, None), + "settings" : (None, None), ## Switches "sticky-on" : (None, None), @@ -611,7 +616,6 @@ class GuiIcons: the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. """ - logger.debug("Loading icon theme files") self.themeMap = {} @@ -748,7 +752,7 @@ class GuiIcons: ## def _loadIcon(self, iconKey): - """Load an icon from the assets or theme folder, with a + """Load an icon from the assets or themes folder, with a preference for dark/light icons depending on theme type, if such an icon exists. Prefer svg files over png files. Always returns a QIcon. @@ -786,6 +790,7 @@ class GuiIcons: if os.path.isfile(fbackIcon): logger.verbose("Loading icon '%s' from fallback theme (dark mode)" % iconKey) return QIcon(fbackIcon) + fbackIcon = os.path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey) if os.path.isfile(fbackIcon): logger.verbose("Loading icon '%s' from fallback theme (light mode)" % iconKey) @@ -797,8 +802,8 @@ class GuiIcons: return QIcon() def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault): - """Simple wrapper for the config parser check for entry existing - before arrempting to load. + """Simple wrapper for the config parser to check that the entry + exists before attempting to load it. """ if confParser.has_section(cnfSec): if confParser.has_option(cnfSec, cnfName): diff --git a/nw/guimain.py b/nw/guimain.py index aa5847b3..a6b1b9d9 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -32,11 +32,11 @@ import os from datetime import datetime from time import time -from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot +from PyQt5.QtCore import Qt, QTimer, QSize, QThreadPool, pyqtSlot from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor from PyQt5.QtWidgets import ( qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, - QMessageBox, QDialog, QTabWidget + QMessageBox, QDialog, QTabWidget, QToolBar, QAction ) from nw.gui import ( @@ -112,7 +112,7 @@ class GuiMain(QMainWindow): self.statusIcons = [] self.importIcons = [] - # Project Tabs : Project / Novel + # Project Tree Tabs self.projTabs = QTabWidget() self.projTabs.setTabPosition(QTabWidget.South) self.projTabs.setStyleSheet("QTabWidget::pane {border: 0;};") @@ -124,6 +124,30 @@ class GuiMain(QMainWindow): tabFont.setPointSize(round(0.9*self.theTheme.fontPointSize)) self.projTabs.tabBar().setFont(tabFont) + # Project Tree Action Buttons + btnSize = round(0.7*self.theTheme.fontPixelSize) + self.treeButtons = QToolBar() + self.treeButtons.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.treeButtons.setIconSize(QSize(btnSize, btnSize)) + self.treeButtons.setContentsMargins(0, 0, 0, 0) + self.treeButtons.setStyleSheet(r"QToolBar {padding: 0;}") + self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner) + + self.projDetailsBtn = QAction("Project Details") + self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines")) + self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog()) + self.treeButtons.addAction(self.projDetailsBtn) + + self.projStatsBtn = QAction("Project Statistics") + self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats")) + self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog()) + self.treeButtons.addAction(self.projStatsBtn) + + self.projSettingsBtn = QAction("Project Settings") + self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings")) + self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog()) + self.treeButtons.addAction(self.projSettingsBtn) + # Project Tree View self.treePane = QWidget() self.treeBox = QVBoxLayout() @@ -269,11 +293,18 @@ class GuiMain(QMainWindow): def clearGUI(self): """Wrapper function to clear all sub-elements of the main GUI. """ + # Project Area self.treeView.clearTree() self.novelView.clearTree() + self.treeMeta.clearDetails() + + # Work Area self.docEditor.clearEditor() self.closeDocViewer() + + # General self.statusBar.clearStatus() + return True def initMain(self): From 84878315d54913543211b178161383f2abe23465 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 14 Jan 2021 18:58:11 +0100 Subject: [PATCH 14/15] Minor GUI tweaks to Project Details and related items --- nw/gui/mainmenu.py | 3 +++ nw/gui/projdetails.py | 4 ++-- nw/guimain.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 77677e21..b22502d1 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -212,6 +212,9 @@ class GuiMainMenu(QMenuBar): self.aCloseProject.triggered.connect(lambda: self.theParent.closeProject(False)) self.projMenu.addAction(self.aCloseProject) + # Project > Separator + self.projMenu.addSeparator() + # Project > Project Settings self.aProjectSettings = QAction("Project Settings", self) self.aProjectSettings.setStatusTip("Project settings") diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index 32131002..ebf69d78 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -56,8 +56,8 @@ class GuiProjectDetails(PagedDialog): self.setWindowTitle("Project Details") - wW = self.mainConf.pxInt(570) - wH = self.mainConf.pxInt(375) + wW = self.mainConf.pxInt(600) + wH = self.mainConf.pxInt(425) self.setMinimumWidth(wW) self.setMinimumHeight(wH) diff --git a/nw/guimain.py b/nw/guimain.py index a6b1b9d9..f141b8e5 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -138,7 +138,7 @@ class GuiMain(QMainWindow): self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog()) self.treeButtons.addAction(self.projDetailsBtn) - self.projStatsBtn = QAction("Project Statistics") + self.projStatsBtn = QAction("Writing Statistics") self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats")) self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog()) self.treeButtons.addAction(self.projStatsBtn) From fb81c7e8e0aaedcdcf3570b26133359420450e59 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 14 Jan 2021 20:09:18 +0100 Subject: [PATCH 15/15] Minimal docs update, and add test for Project Details --- docs/source/int_interface.rst | 1 + docs/source/write_projects.rst | 12 ++--- nw/gui/projdetails.py | 46 ++++++++-------- tests/test_gui_projdetails.py | 96 ++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 tests/test_gui_projdetails.py diff --git a/docs/source/int_interface.rst b/docs/source/int_interface.rst index dcbf5fd9..d8f91047 100644 --- a/docs/source/int_interface.rst +++ b/docs/source/int_interface.rst @@ -406,6 +406,7 @@ Most features are available as keyboard shortcuts. These are as follows: ":kbd:`F11`", "Activate full screen mode." ":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)" + ":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog." ":kbd:`Return`", "If in the project tree, open a document for editing." .. note:: diff --git a/docs/source/write_projects.rst b/docs/source/write_projects.rst index dbc19691..e00dd234 100644 --- a/docs/source/write_projects.rst +++ b/docs/source/write_projects.rst @@ -15,7 +15,9 @@ A list of recently opened projects is maintained, and displayed in the :guilabel dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key. Project-specific settings are available in :guilabel:`Project Settings` in the :guilabel:`Project` -menu. See further details below in the :ref:`a_proj_settings` section. +menu. See further details below in the :ref:`a_proj_settings` section. Details about the project, +including word counts, and a table of contents with word and page counts, is available through the +:guilabel:`Project Details` dialog. .. _a_proj_roots: @@ -249,14 +251,6 @@ override the default spell checking language here. You can also override the aut setting. -Details Tab ------------ - -This tab presents an overview of technical meta data for the project. It states where on your file -system the project is saved, how may times it has been saved, how many folders and documents it -contains, and how many words exist in the entire project. - - Status and Importance Tabs -------------------------- diff --git a/nw/gui/projdetails.py b/nw/gui/projdetails.py index ebf69d78..716177af 100644 --- a/nw/gui/projdetails.py +++ b/nw/gui/projdetails.py @@ -261,19 +261,19 @@ class GuiProjectDetailsContents(QWidget): # Contents Tree # ============= - self.chTree = QTreeWidget() - self.chTree.setIconSize(QSize(iPx, iPx)) - self.chTree.setIndentation(0) - self.chTree.setColumnCount(6) - self.chTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""]) + self.tocTree = QTreeWidget() + self.tocTree.setIconSize(QSize(iPx, iPx)) + self.tocTree.setIndentation(0) + self.tocTree.setColumnCount(6) + self.tocTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""]) - treeHeadItem = self.chTree.headerItem() + treeHeadItem = self.tocTree.headerItem() 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.chTree.header() + treeHeader = self.tocTree.header() treeHeader.setStretchLastSection(True) treeHeader.setMinimumSectionSize(hPx) @@ -283,12 +283,12 @@ class GuiProjectDetailsContents(QWidget): wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60)) wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90)) - self.chTree.setColumnWidth(0, wCol0) - self.chTree.setColumnWidth(1, wCol1) - self.chTree.setColumnWidth(2, wCol2) - self.chTree.setColumnWidth(3, wCol3) - self.chTree.setColumnWidth(4, wCol4) - self.chTree.setColumnWidth(5, hPx) + 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 # ======= @@ -334,7 +334,7 @@ class GuiProjectDetailsContents(QWidget): # ======== self.outerBox = QVBoxLayout() - self.outerBox.addWidget(self.chTree) + self.outerBox.addWidget(self.tocTree) self.outerBox.addLayout(self.optionsBox) self.setLayout(self.outerBox) @@ -348,11 +348,11 @@ class GuiProjectDetailsContents(QWidget): """Return the column widths for the tree columns. """ retVals = [ - self.chTree.columnWidth(0), - self.chTree.columnWidth(1), - self.chTree.columnWidth(2), - self.chTree.columnWidth(3), - self.chTree.columnWidth(4), + self.tocTree.columnWidth(0), + self.tocTree.columnWidth(1), + self.tocTree.columnWidth(2), + self.tocTree.columnWidth(3), + self.tocTree.columnWidth(4), ] return retVals @@ -389,19 +389,21 @@ class GuiProjectDetailsContents(QWidget): pTotal += pCount theList.append((tLevel, tTitle, wCount, pCount)) - self.chTree.clear() + self.tocTree.clear() for tLevel, tTitle, wCount, pCount in theList: newItem = QTreeWidgetItem() if tLevel == "H2": tTitle = nwUnicode.U_ENSP+tTitle + pgProg = 100.0*(tPages - 1)/pTotal if pTotal > 0 else 0.0 + newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_%s" % tLevel.lower())) 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, f"{tPages:n}") - newItem.setText(self.C_PROG, f"{100*(tPages - 1)/pTotal:.1f}{nwUnicode.U_THSP}%") + newItem.setText(self.C_PROG, f"{pgProg:.1f}{nwUnicode.U_THSP}%") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) @@ -410,7 +412,7 @@ class GuiProjectDetailsContents(QWidget): tPages += pCount - self.chTree.addTopLevelItem(newItem) + self.tocTree.addTopLevelItem(newItem) return diff --git a/tests/test_gui_projdetails.py b/tests/test_gui_projdetails.py new file mode 100644 index 00000000..5af043a1 --- /dev/null +++ b/tests/test_gui_projdetails.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +"""novelWriter Writing Stats Dialog Class Tester +""" + +import pytest + +from tools import getGuiItem + +from PyQt5.QtWidgets import QAction, QMessageBox + +from nw.gui import GuiProjectDetails +from nw.constants import nwUnicode + +keyDelay = 2 +typeDelay = 1 +stepDelay = 20 + +@pytest.mark.gui +def testGuiProjDetails_Dialog(qtbot, monkeypatch, nwGUI, nwLipsum): + """Test the full writing stats tool. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "warning", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) + + # Create a project to work on + assert nwGUI.openProject(nwLipsum) + assert nwGUI.rebuildIndex(beQuiet=True) + qtbot.wait(100) + + # Open the Writing Stats dialog + nwGUI.mainConf.lastPath = "" + nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000) + + projDet = getGuiItem("GuiProjectDetails") + assert isinstance(projDet, GuiProjectDetails) + qtbot.wait(stepDelay) + + # 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"{nwGUI.theProject.saveCount:n}" + + assert projDet.tabMain.projPathVal.text() == nwLipsum + + # Contents Page + # ============= + + tocTab = projDet.tabContents + tocTree = tocTab.tocTree + assert tocTree.topLevelItemCount() == 7 + assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" + assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == nwUnicode.U_ENSP+"Prologue" + assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" + assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == nwUnicode.U_ENSP+"Chapter One" + assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == nwUnicode.U_ENSP+"Chapter Two" + assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" + + # Count Pages + tocTab.wpValue.setValue(100) + tocTab.dblValue.setChecked(False) + tocTab._populateTree() + + thePages = [1, 2, 1, 1, 11, 17, 0] + thePage = [1, 2, 4, 5, 6, 17, 34] + for i in range(7): + assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == f"{thePages[i]:n}" + assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == f"{thePage[i]:n}" + + tocTab.dblValue.setChecked(True) + tocTab._populateTree() + + thePages = [2, 2, 2, 2, 12, 18, 0] + thePage = [1, 3, 5, 7, 9, 21, 39] + for i in range(7): + assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == f"{thePages[i]:n}" + assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == f"{thePage[i]:n}" + + # qtbot.stopForInteraction() + + # Clean Up + projDet._doClose() + nwGUI.closeMain() + monkeypatch.undo() + +# END Test testGuiProjDetails_Dialog