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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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