Merge pull request #555 from vkbo/project_details

Project Details Dialog
This commit is contained in:
Veronica K. Berglyd Olsen
2021-01-14 20:14:22 +01:00
committed by GitHub
30 changed files with 1041 additions and 141 deletions
+1
View File
@@ -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::
+3 -9
View File
@@ -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
--------------------------
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg1989">
<metadata
id="metadata1995">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1993" />
<path
d="M 8.3268645,20.0768 8.9719703,22.448 C 9.2032354,23.3008 10.175241,24 11.131598,24 h 1.738831 c 0.956356,0 1.928363,-0.6992 2.159627,-1.552 l 0.645108,-2.3712 c 0.231264,-0.8528 1.173709,-1.3536 2.091812,-1.1104 l 2.550866,0.672 c 0.91984,0.2416 2.06573,-0.1824 2.542169,-0.9456 l 0.869415,-1.3872 c 0.476441,-0.7632 0.307773,-1.8864 -0.380803,-2.4992 l -1.909236,-1.6976 c -0.688578,-0.6128 -0.688578,-1.6128 0.0017,-2.224 L 23.346891,9.1872 C 24.035507,8.5759996 24.205912,7.4527996 23.729472,6.6895996 L 22.858272,5.3024 C 22.380094,4.5392 21.235943,4.1152 20.317842,4.3568 l -2.550866,0.672 C 16.847134,5.2704 15.906428,4.7712 15.673424,3.9184 L 15.030056,1.5504 C 14.798792,0.69919996 13.826785,0 12.870429,0 H 11.131598 C 10.175241,0 9.2032354,0.69919996 8.9719703,1.552 L 8.3286034,3.92 C 8.0955994,4.7728 7.1548921,5.2736 6.2350507,5.032 L 3.684186,4.36 C 2.7643447,4.1168 1.6184552,4.5424 1.1420155,5.304 L 0.27260019,6.6912 c -0.47643954,0.7632 -0.307773,1.8864 0.38254277,2.4991996 L 2.5609015,10.8848 c 0.6868381,0.6128 0.6868381,1.6128 0,2.2256 L 0.65166536,14.808 c -0.68683817,0.6128 -0.85898242,1.736 -0.38080407,2.4992 l 0.87115421,1.3872 c 0.4764397,0.7632 1.6223292,1.1872 2.5421705,0.9456 l 2.5508647,-0.672 c 0.9181025,-0.2448 1.8605487,0.256 2.0918138,1.1088 z M 12.001014,8.8000004 c 1.919668,0 3.477662,1.4319996 3.477662,3.1999996 0,1.7664 -1.557994,3.2 -3.477662,3.2 -1.91967,0 -3.4776615,-1.4336 -3.4776615,-3.2 0,-1.768 1.5579915,-3.1999996 3.4776615,-3.1999996 z"
id="path1987"
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.66797" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg1989">
<metadata
id="metadata1995">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1993" />
<path
d="M 8.3268645,20.0768 8.9719703,22.448 C 9.2032354,23.3008 10.175241,24 11.131598,24 h 1.738831 c 0.956356,0 1.928363,-0.6992 2.159627,-1.552 l 0.645108,-2.3712 c 0.231264,-0.8528 1.173709,-1.3536 2.091812,-1.1104 l 2.550866,0.672 c 0.91984,0.2416 2.06573,-0.1824 2.542169,-0.9456 l 0.869415,-1.3872 c 0.476441,-0.7632 0.307773,-1.8864 -0.380803,-2.4992 l -1.909236,-1.6976 c -0.688578,-0.6128 -0.688578,-1.6128 0.0017,-2.224 L 23.346891,9.1872 C 24.035507,8.5759996 24.205912,7.4527996 23.729472,6.6895996 L 22.858272,5.3024 C 22.380094,4.5392 21.235943,4.1152 20.317842,4.3568 l -2.550866,0.672 C 16.847134,5.2704 15.906428,4.7712 15.673424,3.9184 L 15.030056,1.5504 C 14.798792,0.69919996 13.826785,0 12.870429,0 H 11.131598 C 10.175241,0 9.2032354,0.69919996 8.9719703,1.552 L 8.3286034,3.92 C 8.0955994,4.7728 7.1548921,5.2736 6.2350507,5.032 L 3.684186,4.36 C 2.7643447,4.1168 1.6184552,4.5424 1.1420155,5.304 L 0.27260019,6.6912 c -0.47643954,0.7632 -0.307773,1.8864 0.38254277,2.4991996 L 2.5609015,10.8848 c 0.6868381,0.6128 0.6868381,1.6128 0,2.2256 L 0.65166536,14.808 c -0.68683817,0.6128 -0.85898242,1.736 -0.38080407,2.4992 l 0.87115421,1.3872 c 0.4764397,0.7632 1.6223292,1.1872 2.5421705,0.9456 l 2.5508647,-0.672 c 0.9181025,-0.2448 1.8605487,0.256 2.0918138,1.1088 z M 12.001014,8.8000004 c 1.919668,0 3.477662,1.4319996 3.477662,3.1999996 0,1.7664 -1.557994,3.2 -3.477662,3.2 -1.91967,0 -3.4776615,-1.4336 -3.4776615,-3.2 0,-1.768 1.5579915,-3.1999996 3.4776615,-3.1999996 z"
id="path1987"
style="fill:#000000;fill-opacity:0.721569;stroke-width:1.66797" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1901" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -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
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -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
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1901" />
<path
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -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
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1901" />
<path
style="fill:#000000;fill-opacity:0.78039217;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -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
+3 -3
View File
@@ -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)
+7 -3
View File
@@ -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 = "&nbsp;"
H_THNSP = "&thinsp;"
H_THSP = "&thinsp;"
H_THNBSP = "&#8239;"
H_ENSP = "&ensp;"
H_EMSP = "&emsp;"
## Symbols
H_CHECK = "&#10004;"
+106 -10
View File
@@ -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
@@ -343,6 +345,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
@@ -402,6 +409,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.
"""
@@ -552,16 +582,64 @@ 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 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]
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 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 tHandle in self._listNovelHandles(skipExcluded):
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.
"""
tOrder = []
tData = {}
pKey = None
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._novelIndex[tHandle]):
tKey = "%s:%s" % (tHandle, sTitle)
theData = self._novelIndex[tHandle][sTitle]
iLevel = self.H_LEVEL.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
@@ -647,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
+12 -1
View File
@@ -87,7 +87,18 @@ class OptionState():
"winWidth",
"winHeight",
"replaceColW",
}
},
"GuiProjectDetails": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"widthCol4",
"wordsPerPage",
"clearDouble",
},
}
return
+21
View File
@@ -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
+1 -1
View File
@@ -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,
}
+2
View File
@@ -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",
+1 -1
View File
@@ -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:
+10
View File
@@ -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")
@@ -219,6 +222,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("Shift+F6")
self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog())
self.projMenu.addAction(self.aProjectDetails)
# Project > Separator
self.projMenu.addSeparator()
+419
View File
@@ -0,0 +1,419 @@
# -*- 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 20182021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
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, QGridLayout, QHBoxLayout, QLineEdit
)
from nw.gui.custom import PagedDialog, QSwitch
from nw.constants import nwUnicode
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(600)
wH = self.mainConf.pxInt(425)
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 = GuiProjectDetailsMain(self.theParent, self.theProject)
self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject)
self.addTab(self.tabMain, "Overview")
self.addTab(self.tabContents, "Contents")
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
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())
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])
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
# 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
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.bookTitle.setWordWrap(True)
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.projName.setWordWrap(True)
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
# =====
hCounts = self.theIndex.getNovelTitleCounts()
nwCount = self.theIndex.getNovelWordCount()
self.wordCountLbl = QLabel("<b>Words:</b>")
self.wordCountVal = QLabel(f"{nwCount:n}")
self.chapCountLbl = QLabel("<b>Chapters:</b>")
self.chapCountVal = QLabel(f"{hCounts[2]:n}")
self.sceneCountLbl = QLabel("<b>Scenes:</b>")
self.sceneCountVal = QLabel(f"{hCounts[3]:n}")
self.revCountLbl = QLabel("<b>Revisions:</b>")
self.revCountVal = QLabel(f"{self.theProject.saveCount:n}")
edTime = self.theProject.getCurrentEditTime()
self.editTimeLbl = QLabel("<b>Editing Time:</b>")
self.editTimeVal = QLabel(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
self.statsGrid = QGridLayout()
self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight)
self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft)
self.statsGrid.setHorizontalSpacing(hPx)
self.statsGrid.setVerticalSpacing(vPx)
# Meta
# ====
self.projPathLbl = QLabel("<b>Path:</b>")
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
# ========
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.statsGrid)
self.outerBox.addSpacing(round(0.8*fPx))
self.outerBox.addStretch(1)
self.outerBox.addLayout(self.projPathBox)
self.setLayout(self.outerBox)
return
# END Class GuiProjectDetailsMain
class GuiProjectDetailsContents(QWidget):
C_TITLE = 0
C_WORDS = 1
C_PAGES = 2
C_PAGE = 3
C_PROG = 4
def __init__(self, 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
# Internal
self._theToC = []
iPx = self.theTheme.baseIconSize
hPx = self.mainConf.pxInt(12)
# Contents Tree
# =============
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.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.tocTree.header()
treeHeader.setStretchLastSection(True)
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.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1)
self.tocTree.setColumnWidth(2, wCol2)
self.tocTree.setColumnWidth(3, wCol3)
self.tocTree.setColumnWidth(4, wCol4)
self.tocTree.setColumnWidth(5, hPx)
# Options
# =======
wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350)
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True)
wordsHelp = (
"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."
)
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.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.tocTree)
self.outerBox.addLayout(self.optionsBox)
self.setLayout(self.outerBox)
self._prepareData()
self._populateTree()
return
def getColumnSizes(self):
"""Return the column widths for the tree columns.
"""
retVals = [
self.tocTree.columnWidth(0),
self.tocTree.columnWidth(1),
self.tocTree.columnWidth(2),
self.tocTree.columnWidth(3),
self.tocTree.columnWidth(4),
]
return retVals
##
# 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.
"""
dblPages = self.dblValue.isChecked()
wpPage = self.wpValue.value()
tPages = 1
pTotal = 0
theList = []
for _, tLevel, tTitle, wCount in self._theToC:
pCount = math.ceil(wCount/wpPage)
if dblPages:
pCount += pCount%2
pTotal += pCount
theList.append((tLevel, tTitle, wCount, pCount))
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"{pgProg:.1f}{nwUnicode.U_THSP}%")
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.tocTree.addTopLevelItem(newItem)
return
# END Class GuiProjectDetailsContents
+3 -98
View File
@@ -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("<b>Project Details</b>")
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("<b>Project Stats</b>")
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)
+13 -7
View File
@@ -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 = {
@@ -529,6 +533,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"),
@@ -562,6 +567,7 @@ class GuiIcons:
"reference" : (None, None),
"backward" : (None, None),
"forward" : (None, None),
"settings" : (None, None),
## Switches
"sticky-on" : (None, None),
@@ -610,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 = {}
@@ -747,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.
@@ -785,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)
@@ -796,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):
+42 -5
View File
@@ -32,19 +32,19 @@ 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 (
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, nwLists
@@ -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("Writing 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()
@@ -922,6 +946,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.
"""
@@ -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",
+77 -1
View File
@@ -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
+21
View File
@@ -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
+1 -1
View File
@@ -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)
+96
View File
@@ -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