Merge pull request #555 from vkbo/project_details
Project Details Dialog
This commit is contained in:
@@ -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::
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = "✔"
|
||||
|
||||
+106
-10
@@ -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
@@ -87,7 +87,18 @@ class OptionState():
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"replaceColW",
|
||||
}
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"widthCol0",
|
||||
"widthCol1",
|
||||
"widthCol2",
|
||||
"widthCol3",
|
||||
"widthCol4",
|
||||
"wordsPerPage",
|
||||
"clearDouble",
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 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 <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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user