Merge branch 'main' into release_1.0rc2
@@ -5,20 +5,12 @@ title: ""
|
|||||||
labels: bug
|
labels: bug
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the Bug**
|
**Note**: Issues without a description will not be considered.
|
||||||
A clear and concise description of what the bug is.
|
Please also check if an issue already exists on this problem.
|
||||||
|
Please provide a description covering the following points, if applicable:
|
||||||
|
|
||||||
**To Reproduce**
|
* A clear and concise description of what the bug is.
|
||||||
Steps to reproduce the behaviour:
|
* Steps to reproduce the behaviour
|
||||||
|
* A clear and concise description of what you expected to happen.
|
||||||
**Expected Behaviour**
|
* If applicable, add screenshots to help explain your problem.
|
||||||
A clear and concise description of what you expected to happen.
|
* If the error dialog popped up, copy/paste the content here.
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Error Message**
|
|
||||||
If the error dialog popped up, copy/paste the content here.
|
|
||||||
|
|
||||||
**Additional Context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ title: ""
|
|||||||
labels: enhancement
|
labels: enhancement
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe:**
|
**Note**: Feature requests without a description will not be considered.
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
Please also check if an issue already exists on this feature.
|
||||||
|
Please provide a description covering the following points, if applicable:
|
||||||
|
|
||||||
**Describe the solution you'd like:**
|
* A clear and concise description of what the problem is. Ex. I'm always frustrated when ...
|
||||||
A clear and concise description of what you want to happen.
|
* A clear and concise description of what you want to happen.
|
||||||
|
* A clear and concise description of any alternative solutions or features you've considered.
|
||||||
**Describe alternatives you've considered:**
|
* If applicable, add screenshots to help explain your request.
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context:**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# novelWriter ChangeLog
|
# novelWriter Change Log
|
||||||
|
|
||||||
## Version 1.0 Release Candidate 2 [2020-11-29]
|
## Version 1.0 Release Candidate 2 [2020-11-29]
|
||||||
|
|
||||||
|
|||||||
@@ -58,20 +58,21 @@ from nw.config import Config
|
|||||||
#
|
#
|
||||||
|
|
||||||
__package__ = "nw"
|
__package__ = "nw"
|
||||||
|
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||||
__license__ = "GPLv3"
|
__license__ = "GPLv3"
|
||||||
__author__ = "Veronica Berglyd Olsen"
|
__author__ = "Veronica Berglyd Olsen"
|
||||||
__maintainer__ = "Veronica Berglyd Olsen"
|
__maintainer__ = "Veronica Berglyd Olsen"
|
||||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
|
||||||
__email__ = "code@vkbo.net"
|
__email__ = "code@vkbo.net"
|
||||||
__version__ = "1.0rc2"
|
__version__ = "1.0rc2"
|
||||||
__hexversion__ = "0x010000c2"
|
__hexversion__ = "0x010000c2"
|
||||||
__date__ = "2020-11-29"
|
__date__ = "2020-12-13"
|
||||||
__status__ = "Beta"
|
__status__ = "Beta"
|
||||||
__isstable__ = False
|
__domain__ = "novelwriter.io"
|
||||||
__url__ = "https://novelwriter.io"
|
__url__ = "https://novelwriter.io"
|
||||||
__sourceurl__ = "https://github.com/vkbo/novelWriter"
|
__sourceurl__ = "https://github.com/vkbo/novelWriter"
|
||||||
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
|
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
|
||||||
__domain__ = "novelwriter.io"
|
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
|
||||||
|
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
||||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||||
__credits__ = [
|
__credits__ = [
|
||||||
"Veronica Berglyd Olsen (developer)",
|
"Veronica Berglyd Olsen (developer)",
|
||||||
@@ -246,19 +247,20 @@ def main(sysArgs=None):
|
|||||||
errorCode |= 32
|
errorCode |= 32
|
||||||
|
|
||||||
if errorData:
|
if errorData:
|
||||||
if not testMode:
|
errApp = QApplication([])
|
||||||
errApp = QApplication([])
|
errMsg = QErrorMessage()
|
||||||
errMsg = QErrorMessage()
|
errMsg.resize(500, 300)
|
||||||
errMsg.resize(500, 300)
|
errMsg.showMessage((
|
||||||
errMsg.showMessage((
|
"<h3>A critical error has been encountered</h3>"
|
||||||
"<h3>A critical error has been encountered</h3>"
|
"<p>novelWriter cannot start due to the following issues:<p>"
|
||||||
"<p>novelWriter cannot start due to the following issues:<p>"
|
"<p> - %s</p>"
|
||||||
"<p> - %s</p>"
|
"<p>Shutting down ...</p>"
|
||||||
"<p>Shutting down ...</p>"
|
) % (
|
||||||
) % (
|
"<br> - ".join(errorData)
|
||||||
"<br> - ".join(errorData)
|
))
|
||||||
))
|
for errMsg in errorData:
|
||||||
errApp.exec_()
|
logger.critical(errMsg)
|
||||||
|
errApp.exec_()
|
||||||
sys.exit(errorCode)
|
sys.exit(errorCode)
|
||||||
|
|
||||||
# Finish initialising config
|
# Finish initialising config
|
||||||
@@ -293,4 +295,4 @@ def main(sysArgs=None):
|
|||||||
nwGUI = GuiMain()
|
nwGUI = GuiMain()
|
||||||
sys.exit(nwApp.exec_())
|
sys.exit(nwApp.exec_())
|
||||||
|
|
||||||
return
|
# END Function main
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,207 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 270.93333 270.93334"
|
||||||
|
height="1024"
|
||||||
|
width="1024">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient2515">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f7f7f7;stop-opacity:1"
|
||||||
|
offset="0"
|
||||||
|
id="stop2511" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e5e5e5;stop-opacity:1"
|
||||||
|
offset="1"
|
||||||
|
id="stop2513" />
|
||||||
|
</linearGradient>
|
||||||
|
<rect
|
||||||
|
x="60.484347"
|
||||||
|
y="205.79962"
|
||||||
|
width="130.57787"
|
||||||
|
height="65.785888"
|
||||||
|
id="rect852" />
|
||||||
|
<rect
|
||||||
|
id="rect1474"
|
||||||
|
height="28.631245"
|
||||||
|
width="177.70083"
|
||||||
|
y="18.019813"
|
||||||
|
x="13.463703" />
|
||||||
|
<rect
|
||||||
|
id="rect852-7"
|
||||||
|
height="65.785889"
|
||||||
|
width="130.57787"
|
||||||
|
y="205.79962"
|
||||||
|
x="60.484348" />
|
||||||
|
<rect
|
||||||
|
id="rect1544"
|
||||||
|
height="65.785889"
|
||||||
|
width="130.57787"
|
||||||
|
y="205.79962"
|
||||||
|
x="60.484348" />
|
||||||
|
<rect
|
||||||
|
x="60.484348"
|
||||||
|
y="205.79962"
|
||||||
|
width="130.57787"
|
||||||
|
height="65.785889"
|
||||||
|
id="rect1570" />
|
||||||
|
<rect
|
||||||
|
x="13.463703"
|
||||||
|
y="18.019813"
|
||||||
|
width="177.70083"
|
||||||
|
height="28.631245"
|
||||||
|
id="rect1587" />
|
||||||
|
<filter
|
||||||
|
id="filter1444"
|
||||||
|
style="color-interpolation-filters:sRGB">
|
||||||
|
<feFlood
|
||||||
|
id="feFlood1434"
|
||||||
|
result="flood"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
flood-opacity="0.498039" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1436"
|
||||||
|
result="composite1"
|
||||||
|
operator="in"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
in="flood" />
|
||||||
|
<feGaussianBlur
|
||||||
|
id="feGaussianBlur1438"
|
||||||
|
result="blur"
|
||||||
|
stdDeviation="3"
|
||||||
|
in="composite1" />
|
||||||
|
<feOffset
|
||||||
|
id="feOffset1440"
|
||||||
|
result="offset"
|
||||||
|
dy="6"
|
||||||
|
dx="6" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1442"
|
||||||
|
result="composite2"
|
||||||
|
operator="over"
|
||||||
|
in2="offset"
|
||||||
|
in="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
id="filter1456"
|
||||||
|
style="color-interpolation-filters:sRGB">
|
||||||
|
<feFlood
|
||||||
|
id="feFlood1446"
|
||||||
|
result="flood"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
flood-opacity="0.498039" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1448"
|
||||||
|
result="composite1"
|
||||||
|
operator="in"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
in="flood" />
|
||||||
|
<feGaussianBlur
|
||||||
|
id="feGaussianBlur1450"
|
||||||
|
result="blur"
|
||||||
|
stdDeviation="3"
|
||||||
|
in="composite1" />
|
||||||
|
<feOffset
|
||||||
|
id="feOffset1452"
|
||||||
|
result="offset"
|
||||||
|
dy="6"
|
||||||
|
dx="6" />
|
||||||
|
<feComposite
|
||||||
|
id="feComposite1454"
|
||||||
|
result="composite2"
|
||||||
|
operator="over"
|
||||||
|
in2="offset"
|
||||||
|
in="SourceGraphic" />
|
||||||
|
</filter>
|
||||||
|
<radialGradient
|
||||||
|
xlink:href="#linearGradient2515"
|
||||||
|
id="radialGradient2517"
|
||||||
|
cx="605.42725"
|
||||||
|
cy="623.97186"
|
||||||
|
fx="605.42725"
|
||||||
|
fy="623.97186"
|
||||||
|
r="410"
|
||||||
|
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
</defs>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<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>
|
||||||
|
<g
|
||||||
|
id="layer3"
|
||||||
|
style="display:inline">
|
||||||
|
<path
|
||||||
|
clip-path="none"
|
||||||
|
transform="scale(0.26458333)"
|
||||||
|
d="m 192,22 c -44.32,0 -80,35.680002 -80,80 v 820 c 0,44.32002 35.68,80 80,80 h 640 c 44.31999,0 80,-35.67998 80,-80 V 213.44727 L 720.55469,22 Z"
|
||||||
|
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect1481" />
|
||||||
|
<path
|
||||||
|
id="path1490"
|
||||||
|
style="display:inline;fill:#e0e0e0;fill-opacity:1;stroke:#999999;stroke-width:5.29167;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 241.30037,56.474215 190.64713,5.8204593 V 35.307549 c 0,11.726333 9.44034,21.166666 21.16667,21.166666 z" />
|
||||||
|
<g
|
||||||
|
aria-label=".nwx"
|
||||||
|
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
|
||||||
|
id="text850"
|
||||||
|
style="font-size:5.64444px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect852);display:inline;fill:#666666;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 63.088865,241.76928 q 0,-1.36426 0.909505,-2.27376 0.930175,-0.93018 2.480467,-0.93018 1.550291,0 2.459796,0.93018 0.909504,0.9095 0.909504,2.27376 0,1.34359 -0.909504,2.27376 -0.909505,0.9095 -2.459796,0.9095 -1.550292,0 -2.480467,-0.9095 -0.909505,-0.93017 -0.909505,-2.27376 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||||
|
id="path856" />
|
||||||
|
<path
|
||||||
|
d="m 84.75161,226.78312 q -2.542479,0 -3.782712,2.14974 v 15.79231 h -5.973791 v -22.36554 h 5.601721 l 0.206705,2.56314 q 2.397785,-2.97656 6.428544,-2.97656 3.162595,0 5.105627,1.88103 1.943033,1.88102 1.963703,6.55256 v 14.34537 h -5.994461 v -14.26269 q 0,-2.14973 -0.930175,-2.91454 -0.909505,-0.76482 -2.625161,-0.76482 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||||
|
id="path858" />
|
||||||
|
<path
|
||||||
|
d="m 126.96089,222.35963 -5.68441,22.36554 h -5.00227 l -4.25814,-14.07665 -4.23746,14.07665 h -4.9816 l -5.705078,-22.36554 h 5.767088 l 2.95589,14.22134 4.05143,-14.22134 h 4.32014 l 4.09277,14.20067 2.91455,-14.20067 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||||
|
id="path860" />
|
||||||
|
<path
|
||||||
|
d="m 134.77436,222.35963 3.65869,6.65592 3.76204,-6.65592 h 6.3872 l -6.36653,10.93472 6.63525,11.43082 h -6.3872 l -4.01009,-7.04866 -3.94808,7.04866 h -6.42854 l 6.63525,-11.43082 -6.36654,-10.93472 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||||
|
id="path862" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g2509"
|
||||||
|
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)">
|
||||||
|
<path
|
||||||
|
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
|
||||||
|
id="rect1406-1"
|
||||||
|
style="display:inline;fill:#a11212;fill-opacity:1;stroke:#7e1b1b;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1444)"
|
||||||
|
d="m 145.18984,56.414257 c -44.32,0 -79.999998,35.68 -79.999998,80.000003 v 199.99805 a 279.99999,279.99999 0 0 0 0,0.002 279.99999,279.99999 0 0 0 0,0.0195 v 199.98242 c 0,44.31999 35.679998,80 79.999998,80 44.32,0 80,-35.68001 80,-80 v -200 a 120,120 0 0 1 120,-120 120,120 0 0 1 120,120 v 282.83592 h 160 V 336.41621 a 279.99999,279.99999 0 0 0 -280,-280.000003 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 c -14.4245,-20.75262 -38.42829,-34.29883 -65.72461,-34.29883 z M 376.65078,619.64082 c -0.62174,0.0972 -1.2172,0.22317 -1.7793,0.37696 0.5621,-0.15379 1.15756,-0.27973 1.7793,-0.37696 z m -1.7793,0.37696 c -0.5621,0.15378 -1.09071,0.33534 -1.58008,0.53906 0.48937,-0.20372 1.01798,-0.38528 1.58008,-0.53906 z m 338.85742,-0.37696 c 0.62174,0.0972 1.2172,0.22317 1.7793,0.37696 -0.5621,-0.15379 -1.15756,-0.27973 -1.7793,-0.37696 z m 1.7793,0.37696 c 0.5621,0.15378 1.09071,0.33534 1.58008,0.53906 -0.48938,-0.20372 -1.01798,-0.38528 -1.58008,-0.53906 z" />
|
||||||
|
<path
|
||||||
|
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
|
||||||
|
id="path897"
|
||||||
|
style="display:inline;fill:#730d0d;fill-opacity:1;stroke:#590d0d;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1456)"
|
||||||
|
d="m 465.18984,619.25215 v 0.16406 h -85.58398 c -5.49302,0 -9.91602,2.2329 -9.91602,5.00586 0,0.52294 0.15913,1.02696 0.45117,1.5 h -0.44531 c 0.30916,2.03607 1.24102,3.99791 2.81641,5.57227 l 165.59961,165.49414 c 3.92061,3.91808 10.23371,3.91812 14.15429,0 L 717.86757,631.49434 c 1.57569,-1.57463 2.50725,-3.53578 2.81641,-5.57227 h -0.44531 c 0.29216,-0.47304 0.45117,-0.97706 0.45117,-1.5 0,-2.77296 -4.42304,-5.00586 -9.91602,-5.00586 h -85.58398 v -0.16406 z" />
|
||||||
|
<rect
|
||||||
|
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1"
|
||||||
|
id="rect914"
|
||||||
|
width="6.7583966"
|
||||||
|
height="82.482399"
|
||||||
|
x="-118.27303"
|
||||||
|
y="1.2969339"
|
||||||
|
rx="3.3791983"
|
||||||
|
ry="3.6658845" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>Release Notes for 1.0 RC1</h2>
|
||||||
|
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
|
||||||
|
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
|
||||||
|
few minor tweaks have been made to the GUI.</p>
|
||||||
|
<p>A number of features and tools are now automatically switched off when there is no project or
|
||||||
|
document open for those features to act upon. Previously, this was a bit inconsistent, although no
|
||||||
|
serious bugs have been reported or encountered.</p>
|
||||||
|
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
|
||||||
|
are a couple of noticeable changes.</p>
|
||||||
|
<h3>Typewriter Mode</h3>
|
||||||
|
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
|
||||||
|
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
|
||||||
|
scroll the document up when the cursor skips to a new line while typing (or down in case of
|
||||||
|
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
|
||||||
|
It improves the writing experience as the current active line will stay at the same eye height
|
||||||
|
level on the screen.</p>
|
||||||
|
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
|
||||||
|
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
|
||||||
|
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
|
||||||
|
top where this feature is activated. These changes makes it more flexible in terms of where the
|
||||||
|
focus is in the editor. The feature can be controlled from the main Preferences.</p>
|
||||||
|
<h3>Switching Syntax Theme</h3>
|
||||||
|
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
|
||||||
|
Previously, changing the theme would only half-way update the document, header and footer
|
||||||
|
background and text colours. The new settings would not be fully applied until the application was
|
||||||
|
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
|
||||||
|
one you want.</p>
|
||||||
|
<p>Switching main GUI theme still requires a restart.</p>
|
||||||
|
<p><i>The full changelog is available
|
||||||
|
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
|||||||
background = 255, 255, 255
|
background = 255, 255, 255
|
||||||
text = 0, 0, 0
|
text = 0, 0, 0
|
||||||
link = 0, 0, 200
|
link = 0, 0, 200
|
||||||
headertext = 0, 100, 00
|
headertext = 0, 100, 0
|
||||||
headertag = 50, 100, 50
|
headertag = 50, 100, 50
|
||||||
emphasis = 150, 110, 30
|
emphasis = 150, 110, 30
|
||||||
straightquotes = 200, 0, 0
|
straightquotes = 200, 0, 0
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[Main]
|
||||||
|
name = Grey Dark
|
||||||
|
author = Veronica Berglyd Olsen
|
||||||
|
credit = Veronica Berglyd Olsen
|
||||||
|
url = https://github.com/vkbo/novelWriter
|
||||||
|
license = CC BY-SA 4.0
|
||||||
|
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
[Syntax]
|
||||||
|
background = 54, 54, 54
|
||||||
|
text = 200, 200, 200
|
||||||
|
link = 200, 200, 200
|
||||||
|
headertext = 225, 225, 225
|
||||||
|
headertag = 225, 225, 225
|
||||||
|
emphasis = 200, 200, 200
|
||||||
|
straightquotes = 200, 200, 200
|
||||||
|
doublequotes = 200, 200, 200
|
||||||
|
singlequotes = 200, 200, 200
|
||||||
|
hidden = 150, 150, 150
|
||||||
|
keyword = 225, 225, 225
|
||||||
|
value = 200, 200, 200
|
||||||
|
spellcheckline = 200, 46, 0
|
||||||
|
tagerror = 46, 200, 0
|
||||||
|
replacetag = 225, 225, 225
|
||||||
|
modifier = 225, 225, 225
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[Main]
|
||||||
|
name = Grey Light
|
||||||
|
author = Veronica Berglyd Olsen
|
||||||
|
credit = Veronica Berglyd Olsen
|
||||||
|
url = https://github.com/vkbo/novelWriter
|
||||||
|
license = CC BY-SA 4.0
|
||||||
|
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
[Syntax]
|
||||||
|
background = 255, 255, 255
|
||||||
|
text = 20, 20, 20
|
||||||
|
link = 20, 20, 20
|
||||||
|
headertext = 0, 0, 0
|
||||||
|
headertag = 0, 0, 0
|
||||||
|
emphasis = 20, 20, 20
|
||||||
|
straightquotes = 20, 20, 20
|
||||||
|
doublequotes = 20, 20, 20
|
||||||
|
singlequotes = 20, 20, 20
|
||||||
|
hidden = 100, 100, 100
|
||||||
|
keyword = 0, 0, 0
|
||||||
|
value = 20, 20, 20
|
||||||
|
spellcheckline = 200, 0, 0
|
||||||
|
tagerror = 0, 150, 0
|
||||||
|
replacetag = 0, 0, 0
|
||||||
|
modifier = 0, 0, 0
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import configparser
|
import configparser
|
||||||
|
import shutil
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
from shutil import which
|
|
||||||
|
|
||||||
from PyQt5.Qt import PYQT_VERSION_STR
|
from PyQt5.Qt import PYQT_VERSION_STR
|
||||||
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
||||||
@@ -44,10 +44,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
||||||
CNF_STR = 0
|
CNF_STR = 0
|
||||||
CNF_INT = 1
|
CNF_INT = 1
|
||||||
CNF_BOOL = 2
|
CNF_BOOL = 2
|
||||||
CNF_LIST = 3
|
CNF_S_LST = 3
|
||||||
|
CNF_I_LST = 4
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ class Config:
|
|||||||
self.guiFont = "" # Defaults to system default font
|
self.guiFont = "" # Defaults to system default font
|
||||||
self.guiFontSize = 11
|
self.guiFontSize = 11
|
||||||
self.guiScale = 1.0 # Set automatically by Theme class
|
self.guiScale = 1.0 # Set automatically by Theme class
|
||||||
|
self.lastNotes = "" # The latest release notes that have been shown
|
||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
self.winGeometry = [1200, 650]
|
self.winGeometry = [1200, 650]
|
||||||
@@ -210,12 +212,8 @@ class Config:
|
|||||||
self.osUnknown = True
|
self.osUnknown = True
|
||||||
|
|
||||||
# Other System Info
|
# Other System Info
|
||||||
if self.verQtValue >= 50600:
|
self.hostName = "Unknown"
|
||||||
self.hostName = QSysInfo.machineHostName()
|
self.kernelVer = "Unknown"
|
||||||
self.kernelVer = QSysInfo.kernelVersion()
|
|
||||||
else:
|
|
||||||
self.hostName = "Unknown"
|
|
||||||
self.kernelVer = "Unknown"
|
|
||||||
|
|
||||||
# Packages
|
# Packages
|
||||||
self.hasEnchant = False # The pyenchant package
|
self.hasEnchant = False # The pyenchant package
|
||||||
@@ -320,6 +318,11 @@ class Config:
|
|||||||
self.errData.append(str(e))
|
self.errData.append(str(e))
|
||||||
self.dataPath = None
|
self.dataPath = None
|
||||||
|
|
||||||
|
# Host and Kernel
|
||||||
|
if self.verQtValue >= 50600:
|
||||||
|
self.hostName = QSysInfo.machineHostName()
|
||||||
|
self.kernelVer = QSysInfo.kernelVersion()
|
||||||
|
|
||||||
# Load recent projects cache
|
# Load recent projects cache
|
||||||
self.loadRecentCache()
|
self.loadRecentCache()
|
||||||
|
|
||||||
@@ -380,29 +383,32 @@ class Config:
|
|||||||
self.guiFontSize = self._parseLine(
|
self.guiFontSize = self._parseLine(
|
||||||
cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
|
cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
|
||||||
)
|
)
|
||||||
|
self.lastNotes = self._parseLine(
|
||||||
|
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
|
||||||
|
)
|
||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
cnfSec = "Sizes"
|
cnfSec = "Sizes"
|
||||||
self.winGeometry = self._parseLine(
|
self.winGeometry = self._parseLine(
|
||||||
cnfParse, cnfSec, "geometry", self.CNF_LIST, self.winGeometry
|
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
|
||||||
)
|
)
|
||||||
self.treeColWidth = self._parseLine(
|
self.treeColWidth = self._parseLine(
|
||||||
cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth
|
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
|
||||||
)
|
)
|
||||||
self.projColWidth = self._parseLine(
|
self.projColWidth = self._parseLine(
|
||||||
cnfParse, cnfSec, "projcols", self.CNF_LIST, self.projColWidth
|
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
|
||||||
)
|
)
|
||||||
self.mainPanePos = self._parseLine(
|
self.mainPanePos = self._parseLine(
|
||||||
cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos
|
cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos
|
||||||
)
|
)
|
||||||
self.docPanePos = self._parseLine(
|
self.docPanePos = self._parseLine(
|
||||||
cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos
|
cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos
|
||||||
)
|
)
|
||||||
self.viewPanePos = self._parseLine(
|
self.viewPanePos = self._parseLine(
|
||||||
cnfParse, cnfSec, "viewpane", self.CNF_LIST, self.viewPanePos
|
cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos
|
||||||
)
|
)
|
||||||
self.outlnPanePos = self._parseLine(
|
self.outlnPanePos = self._parseLine(
|
||||||
cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos
|
cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos
|
||||||
)
|
)
|
||||||
self.isFullScreen = self._parseLine(
|
self.isFullScreen = self._parseLine(
|
||||||
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
|
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
|
||||||
@@ -480,10 +486,10 @@ class Config:
|
|||||||
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
|
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
|
||||||
)
|
)
|
||||||
self.fmtSingleQuotes = self._parseLine(
|
self.fmtSingleQuotes = self._parseLine(
|
||||||
cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes
|
cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes
|
||||||
)
|
)
|
||||||
self.fmtDoubleQuotes = self._parseLine(
|
self.fmtDoubleQuotes = self._parseLine(
|
||||||
cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes
|
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
|
||||||
)
|
)
|
||||||
self.spellTool = self._parseLine(
|
self.spellTool = self._parseLine(
|
||||||
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
|
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
|
||||||
@@ -584,6 +590,7 @@ class Config:
|
|||||||
cnfParse.set(cnfSec, "guidark", str(self.guiDark))
|
cnfParse.set(cnfSec, "guidark", str(self.guiDark))
|
||||||
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
|
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
|
||||||
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
|
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
|
||||||
|
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
|
||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
cnfSec = "Sizes"
|
cnfSec = "Sizes"
|
||||||
@@ -896,23 +903,28 @@ class Config:
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _unpackList(self, inStr, listLen, listDefault, castTo=int):
|
|
||||||
"""Unpack a comma separated string of items into a list.
|
|
||||||
"""
|
|
||||||
inData = inStr.split(",")
|
|
||||||
outData = []
|
|
||||||
for i in range(listLen):
|
|
||||||
try:
|
|
||||||
outData.append(castTo(inData[i]))
|
|
||||||
except Exception:
|
|
||||||
outData.append(listDefault[i])
|
|
||||||
return outData
|
|
||||||
|
|
||||||
def _packList(self, inData):
|
def _packList(self, inData):
|
||||||
"""Pack a list of items into a comma separated string.
|
"""Pack a list of items into a comma-separated string.
|
||||||
"""
|
"""
|
||||||
return ", ".join([str(inVal) for inVal in inData])
|
return ", ".join([str(inVal) for inVal in inData])
|
||||||
|
|
||||||
|
def _unpackList(self, inStr, listDefault, cnfType):
|
||||||
|
"""Unpack a comma-separated string of items into a list.
|
||||||
|
"""
|
||||||
|
inData = inStr.split(",")
|
||||||
|
outData = listDefault.copy()
|
||||||
|
for i in range(min(len(inData), len(listDefault))):
|
||||||
|
try:
|
||||||
|
if cnfType == self.CNF_S_LST:
|
||||||
|
outData[i] = inData[i].strip()
|
||||||
|
elif cnfType == self.CNF_I_LST:
|
||||||
|
outData[i] = int(inData[i].strip())
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return outData
|
||||||
|
|
||||||
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
|
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
|
||||||
"""Parse a line and return the correct datatype.
|
"""Parse a line and return the correct datatype.
|
||||||
"""
|
"""
|
||||||
@@ -925,18 +937,24 @@ class Config:
|
|||||||
return cnfParse.getint(cnfSec, cnfName)
|
return cnfParse.getint(cnfSec, cnfName)
|
||||||
elif cnfType == self.CNF_BOOL:
|
elif cnfType == self.CNF_BOOL:
|
||||||
return cnfParse.getboolean(cnfSec, cnfName)
|
return cnfParse.getboolean(cnfSec, cnfName)
|
||||||
elif cnfType == self.CNF_LIST:
|
elif cnfType == self.CNF_I_LST:
|
||||||
return self._unpackList(
|
return self._unpackList(
|
||||||
cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault
|
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST
|
||||||
|
)
|
||||||
|
elif cnfType == self.CNF_S_LST:
|
||||||
|
return self._unpackList(
|
||||||
|
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error("Failed to load value from config file.")
|
logger.error("Failed to load value from config file.")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
return cnfDefault
|
||||||
|
|
||||||
return cnfDefault
|
return cnfDefault
|
||||||
|
|
||||||
def _checkNone(self, checkVal):
|
def _checkNone(self, checkVal):
|
||||||
"""Convert a string to a none type.
|
"""Return a NoneType if the value correspomds to None, otherwise
|
||||||
|
return the value unchanged.
|
||||||
"""
|
"""
|
||||||
if checkVal is None:
|
if checkVal is None:
|
||||||
return None
|
return None
|
||||||
@@ -956,10 +974,8 @@ class Config:
|
|||||||
self.hasEnchant = False
|
self.hasEnchant = False
|
||||||
logger.debug("Checking package 'pyenchant': Missing")
|
logger.debug("Checking package 'pyenchant': Missing")
|
||||||
|
|
||||||
try:
|
assistPath = shutil.which("assistant")
|
||||||
self.hasAssistant = which("assistant")
|
self.hasAssistant = assistPath is not None
|
||||||
except Exception:
|
|
||||||
self.hasAssistant = False
|
|
||||||
if self.hasAssistant:
|
if self.hasAssistant:
|
||||||
logger.debug("Checking executable 'assistant': Ok")
|
logger.debug("Checking executable 'assistant': Ok")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ class NWDoc():
|
|||||||
os.unlink(docPath)
|
os.unlink(docPath)
|
||||||
os.rename(docTemp, docPath)
|
os.rename(docTemp, docPath)
|
||||||
|
|
||||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
if self._theItem is not None:
|
||||||
|
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class NWItem():
|
|||||||
self.itemName = ""
|
self.itemName = ""
|
||||||
self.itemHandle = None
|
self.itemHandle = None
|
||||||
self.itemParent = None
|
self.itemParent = None
|
||||||
self.itemOrder = None
|
self.itemOrder = 0
|
||||||
self.itemType = nwItemType.NO_TYPE
|
self.itemType = nwItemType.NO_TYPE
|
||||||
self.itemClass = nwItemClass.NO_CLASS
|
self.itemClass = nwItemClass.NO_CLASS
|
||||||
self.itemLayout = nwItemLayout.NO_LAYOUT
|
self.itemLayout = nwItemLayout.NO_LAYOUT
|
||||||
@@ -96,20 +96,21 @@ class NWItem():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if "handle" in xItem.attrib:
|
if "handle" in xItem.attrib:
|
||||||
self.itemHandle = xItem.attrib["handle"]
|
self.setHandle(xItem.attrib["handle"])
|
||||||
else:
|
else:
|
||||||
logger.error("XML item entry does not have a handle")
|
logger.error("XML item entry does not have a handle")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if "parent" in xItem.attrib:
|
if "parent" in xItem.attrib:
|
||||||
self.itemParent = xItem.attrib["parent"]
|
self.setParent(xItem.attrib["parent"])
|
||||||
|
|
||||||
|
if "order" in xItem.attrib:
|
||||||
|
self.setOrder(xItem.attrib["order"])
|
||||||
|
|
||||||
retStatus = True
|
retStatus = True
|
||||||
for xValue in xItem:
|
for xValue in xItem:
|
||||||
if xValue.tag == "name":
|
if xValue.tag == "name":
|
||||||
self.setName(xValue.text)
|
self.setName(xValue.text)
|
||||||
elif xValue.tag == "order":
|
|
||||||
self.setOrder(xValue.text)
|
|
||||||
elif xValue.tag == "type":
|
elif xValue.tag == "type":
|
||||||
self.setType(xValue.text)
|
self.setType(xValue.text)
|
||||||
elif xValue.tag == "class":
|
elif xValue.tag == "class":
|
||||||
@@ -156,7 +157,10 @@ class NWItem():
|
|||||||
def setName(self, theName):
|
def setName(self, theName):
|
||||||
"""Set the item name.
|
"""Set the item name.
|
||||||
"""
|
"""
|
||||||
self.itemName = theName.strip()
|
if isinstance(theName, str):
|
||||||
|
self.itemName = theName.strip()
|
||||||
|
else:
|
||||||
|
self.itemName = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
def setHandle(self, theHandle):
|
def setHandle(self, theHandle):
|
||||||
|
|||||||
@@ -176,11 +176,7 @@ class OptionState():
|
|||||||
"""
|
"""
|
||||||
if getGroup in self.theState:
|
if getGroup in self.theState:
|
||||||
if getName in self.theState[getGroup]:
|
if getName in self.theState[getGroup]:
|
||||||
try:
|
return self.theState[getGroup][getName]
|
||||||
return self.theState[getGroup][getName]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
return defaultValue
|
|
||||||
return defaultValue
|
return defaultValue
|
||||||
|
|
||||||
def getString(self, getGroup, getName, defaultValue):
|
def getString(self, getGroup, getName, defaultValue):
|
||||||
@@ -189,11 +185,7 @@ class OptionState():
|
|||||||
"""
|
"""
|
||||||
if getGroup in self.theState:
|
if getGroup in self.theState:
|
||||||
if getName in self.theState[getGroup]:
|
if getName in self.theState[getGroup]:
|
||||||
try:
|
return str(self.theState[getGroup][getName])
|
||||||
return str(self.theState[getGroup][getName])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
return defaultValue
|
|
||||||
return defaultValue
|
return defaultValue
|
||||||
|
|
||||||
def getInt(self, getGroup, getName, defaultValue):
|
def getInt(self, getGroup, getName, defaultValue):
|
||||||
@@ -228,11 +220,7 @@ class OptionState():
|
|||||||
"""
|
"""
|
||||||
if getGroup in self.theState:
|
if getGroup in self.theState:
|
||||||
if getName in self.theState[getGroup]:
|
if getName in self.theState[getGroup]:
|
||||||
try:
|
return bool(self.theState[getGroup][getName])
|
||||||
return bool(self.theState[getGroup][getName])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(str(e))
|
|
||||||
return defaultValue
|
|
||||||
return defaultValue
|
return defaultValue
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -28,12 +28,10 @@
|
|||||||
import nw
|
import nw
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from time import time
|
from time import time
|
||||||
from shutil import make_archive, unpack_archive, copyfile
|
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from nw.core.tree import NWTree
|
from nw.core.tree import NWTree
|
||||||
from nw.core.item import NWItem
|
from nw.core.item import NWItem
|
||||||
@@ -436,26 +434,15 @@ class NWProject():
|
|||||||
xRoot = nwXML.getroot()
|
xRoot = nwXML.getroot()
|
||||||
nwxRoot = xRoot.tag
|
nwxRoot = xRoot.tag
|
||||||
|
|
||||||
appVersion = "Unknown"
|
appVersion = xRoot.attrib.get("appVersion", "Unknown")
|
||||||
hexVersion = "0x0"
|
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
|
||||||
fileVersion = "Unknown"
|
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
|
||||||
self.saveCount = 0
|
|
||||||
self.autoCount = 0
|
|
||||||
|
|
||||||
if "appVersion" in xRoot.attrib:
|
|
||||||
appVersion = xRoot.attrib["appVersion"]
|
|
||||||
if "hexVersion" in xRoot.attrib:
|
|
||||||
hexVersion = xRoot.attrib["hexVersion"]
|
|
||||||
if "fileVersion" in xRoot.attrib:
|
|
||||||
fileVersion = xRoot.attrib["fileVersion"]
|
|
||||||
|
|
||||||
# The following are deprecated and will be removed
|
# The following are deprecated and will be removed
|
||||||
if "saveCount" in xRoot.attrib:
|
# The settings have been moved to the <project> tag
|
||||||
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
|
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
|
||||||
if "autoCount" in xRoot.attrib:
|
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
|
||||||
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
|
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
|
||||||
if "editTime" in xRoot.attrib:
|
|
||||||
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
|
|
||||||
|
|
||||||
logger.verbose("XML root is %s" % nwxRoot)
|
logger.verbose("XML root is %s" % nwxRoot)
|
||||||
logger.verbose("File version is %s" % fileVersion)
|
logger.verbose("File version is %s" % fileVersion)
|
||||||
@@ -483,20 +470,19 @@ class NWProject():
|
|||||||
# parser will lose the autoReplace settings if allowed to
|
# parser will lose the autoReplace settings if allowed to
|
||||||
# read the file. Introduced in version 0.10.
|
# read the file. Introduced in version 0.10.
|
||||||
|
|
||||||
if fileVersion == "1.0" and self.mainConf.showGUI:
|
if fileVersion == "1.0":
|
||||||
msgBox = QMessageBox()
|
msgRes = self.theParent.askQuestion("Old Project Version", (
|
||||||
msgRes = msgBox.question(self.theParent, "Old Project Version", (
|
|
||||||
"The project file and data is created by a novelWriter version "
|
"The project file and data is created by a novelWriter version "
|
||||||
"lower than 0.7. Do you want to upgrade the project to the "
|
"lower than 0.7. Do you want to upgrade the project to the "
|
||||||
"most recent format?<br><br>Note that after the upgrade, you "
|
"most recent format?<br><br>Note that after the upgrade, you "
|
||||||
"cannot open the project with an older version of novelWriter "
|
"cannot open the project with an older version of novelWriter "
|
||||||
"any more, so make sure you have a recent backup."
|
"any more, so make sure you have a recent backup."
|
||||||
))
|
))
|
||||||
if msgRes != QMessageBox.Yes:
|
if not msgRes:
|
||||||
self.clearProject()
|
self.clearProject()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI:
|
elif fileVersion != "1.1" and fileVersion != "1.2":
|
||||||
self.makeAlert((
|
self.makeAlert((
|
||||||
"Unknown or unsupported novelWriter project file format. "
|
"Unknown or unsupported novelWriter project file format. "
|
||||||
"The project cannot be opened by this version of novelWriter. "
|
"The project cannot be opened by this version of novelWriter. "
|
||||||
@@ -510,9 +496,8 @@ class NWProject():
|
|||||||
# Check novelWriter Version
|
# Check novelWriter Version
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
|
if int(hexVersion, 16) > int(nw.__hexversion__, 16):
|
||||||
msgBox = QMessageBox()
|
msgRes = self.theParent.askQuestion("Version Conflict", (
|
||||||
msgRes = msgBox.question(self.theParent, "Version Conflict", (
|
|
||||||
"This project was saved by a newer version of novelWriter, version %s. "
|
"This project was saved by a newer version of novelWriter, version %s. "
|
||||||
"This is version %s. If you continue to open the project, some attributes "
|
"This is version %s. If you continue to open the project, some attributes "
|
||||||
"and settings may not be preserved, but the overall project should be fine. "
|
"and settings may not be preserved, but the overall project should be fine. "
|
||||||
@@ -520,7 +505,7 @@ class NWProject():
|
|||||||
) % (
|
) % (
|
||||||
appVersion, nw.__version__
|
appVersion, nw.__version__
|
||||||
))
|
))
|
||||||
if msgRes != QMessageBox.Yes:
|
if not msgRes:
|
||||||
self.clearProject()
|
self.clearProject()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -835,15 +820,15 @@ class NWProject():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self._clearLockFile()
|
self._clearLockFile()
|
||||||
make_archive(baseName, "zip", self.projPath, ".")
|
shutil.make_archive(baseName, "zip", self.projPath, ".")
|
||||||
self._writeLockFile()
|
self._writeLockFile()
|
||||||
|
logger.info("Backup written to: %s" % archName)
|
||||||
if doNotify:
|
if doNotify:
|
||||||
self.theParent.makeAlert(
|
self.theParent.makeAlert(
|
||||||
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
|
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
|
||||||
nwAlert.INFO
|
nwAlert.INFO
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
logger.info("Backup written to: %s" % archName)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.theParent.makeAlert(
|
self.theParent.makeAlert(
|
||||||
["Could not write backup archive.", str(e)],
|
["Could not write backup archive.", str(e)],
|
||||||
@@ -874,7 +859,7 @@ class NWProject():
|
|||||||
|
|
||||||
self.setProjectPath(projPath, newProject=True)
|
self.setProjectPath(projPath, newProject=True)
|
||||||
try:
|
try:
|
||||||
unpack_archive(pkgSample, projPath)
|
shutil.unpack_archive(pkgSample, projPath)
|
||||||
isSuccess = True
|
isSuccess = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.makeAlert(
|
self.makeAlert(
|
||||||
@@ -887,14 +872,14 @@ class NWProject():
|
|||||||
try:
|
try:
|
||||||
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
|
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
|
||||||
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
|
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
|
||||||
copyfile(srcProj, dstProj)
|
shutil.copyfile(srcProj, dstProj)
|
||||||
|
|
||||||
srcContent = os.path.join(srcSample, "content")
|
srcContent = os.path.join(srcSample, "content")
|
||||||
dstContent = os.path.join(projPath, "content")
|
dstContent = os.path.join(projPath, "content")
|
||||||
for srcFile in os.listdir(srcContent):
|
for srcFile in os.listdir(srcContent):
|
||||||
srcDoc = os.path.join(srcContent, srcFile)
|
srcDoc = os.path.join(srcContent, srcFile)
|
||||||
dstDoc = os.path.join(dstContent, srcFile)
|
dstDoc = os.path.join(dstContent, srcFile)
|
||||||
copyfile(srcDoc, dstDoc)
|
shutil.copyfile(srcDoc, dstDoc)
|
||||||
|
|
||||||
isSuccess = True
|
isSuccess = True
|
||||||
|
|
||||||
@@ -998,11 +983,15 @@ class NWProject():
|
|||||||
"You must set a valid backup path in preferences to use "
|
"You must set a valid backup path in preferences to use "
|
||||||
"the automatic project backup feature."
|
"the automatic project backup feature."
|
||||||
), nwAlert.WARN)
|
), nwAlert.WARN)
|
||||||
|
return False
|
||||||
|
|
||||||
if self.projName == "":
|
if self.projName == "":
|
||||||
self.theParent.makeAlert((
|
self.theParent.makeAlert((
|
||||||
"You must set a valid project name in project settings to "
|
"You must set a valid project name in project settings to "
|
||||||
"use the automatic project backup feature."
|
"use the automatic project backup feature."
|
||||||
), nwAlert.WARN)
|
), nwAlert.WARN)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setSpellCheck(self, theMode):
|
def setSpellCheck(self, theMode):
|
||||||
@@ -1011,12 +1000,15 @@ class NWProject():
|
|||||||
if self.spellCheck != theMode:
|
if self.spellCheck != theMode:
|
||||||
self.spellCheck = theMode
|
self.spellCheck = theMode
|
||||||
self.setProjectChanged(True)
|
self.setProjectChanged(True)
|
||||||
return True
|
return self.spellCheck
|
||||||
|
|
||||||
def setSpellLang(self, theLang):
|
def setSpellLang(self, theLang):
|
||||||
"""Set the project-specific spell check language.
|
"""Set the project-specific spell check language.
|
||||||
"""
|
"""
|
||||||
self.projLang = checkString(theLang, None, True)
|
theLang = checkString(theLang, None, True)
|
||||||
|
if self.projLang != theLang:
|
||||||
|
self.projLang = theLang
|
||||||
|
self.setProjectChanged(True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setAutoOutline(self, theMode):
|
def setAutoOutline(self, theMode):
|
||||||
@@ -1025,7 +1017,7 @@ class NWProject():
|
|||||||
if self.autoOutline != theMode:
|
if self.autoOutline != theMode:
|
||||||
self.autoOutline = theMode
|
self.autoOutline = theMode
|
||||||
self.setProjectChanged(True)
|
self.setProjectChanged(True)
|
||||||
return True
|
return self.autoOutline
|
||||||
|
|
||||||
def setTreeOrder(self, newOrder):
|
def setTreeOrder(self, newOrder):
|
||||||
"""A list representing the linear/flattened order of project
|
"""A list representing the linear/flattened order of project
|
||||||
@@ -1072,7 +1064,7 @@ class NWProject():
|
|||||||
if nwItem.itemStatus in replaceMap.keys():
|
if nwItem.itemStatus in replaceMap.keys():
|
||||||
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
||||||
self.setProjectChanged(True)
|
self.setProjectChanged(True)
|
||||||
return
|
return True
|
||||||
|
|
||||||
def setImportColours(self, newCols):
|
def setImportColours(self, newCols):
|
||||||
"""Update the list of note file importance flags. Also iterate
|
"""Update the list of note file importance flags. Also iterate
|
||||||
@@ -1084,14 +1076,15 @@ class NWProject():
|
|||||||
if nwItem.itemStatus in replaceMap.keys():
|
if nwItem.itemStatus in replaceMap.keys():
|
||||||
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
||||||
self.setProjectChanged(True)
|
self.setProjectChanged(True)
|
||||||
return
|
return True
|
||||||
|
|
||||||
def setAutoReplace(self, autoReplace):
|
def setAutoReplace(self, autoReplace):
|
||||||
"""Update the auto-replace dictionary. This replaces the entire
|
"""Update the auto-replace dictionary. This replaces the entire
|
||||||
dictionary, so alterations have to be made in a copy.
|
dictionary, so alterations have to be made in a copy.
|
||||||
"""
|
"""
|
||||||
self.autoReplace = autoReplace
|
self.autoReplace = autoReplace
|
||||||
return
|
self.setProjectChanged(True)
|
||||||
|
return True
|
||||||
|
|
||||||
def setTitleFormat(self, titleFormat):
|
def setTitleFormat(self, titleFormat):
|
||||||
"""Set the formatting of titles in the project.
|
"""Set the formatting of titles in the project.
|
||||||
@@ -1099,7 +1092,7 @@ class NWProject():
|
|||||||
for valKey, valEntry in titleFormat.items():
|
for valKey, valEntry in titleFormat.items():
|
||||||
if valKey in self.titleFormat:
|
if valKey in self.titleFormat:
|
||||||
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
|
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
|
||||||
return
|
return True
|
||||||
|
|
||||||
def setProjectChanged(self, bValue):
|
def setProjectChanged(self, bValue):
|
||||||
"""Toggle the project changed flag, and propagate the
|
"""Toggle the project changed flag, and propagate the
|
||||||
@@ -1132,13 +1125,11 @@ class NWProject():
|
|||||||
sentItems = []
|
sentItems = []
|
||||||
iterItems = self.projTree.handles()
|
iterItems = self.projTree.handles()
|
||||||
n = 0
|
n = 0
|
||||||
nMax = len(iterItems)
|
nMax = min(len(iterItems), 10000)
|
||||||
while n < nMax:
|
while n < nMax:
|
||||||
tHandle = iterItems[n]
|
tHandle = iterItems[n]
|
||||||
tItem = self.projTree[tHandle]
|
tItem = self.projTree[tHandle]
|
||||||
n += 1
|
n += 1
|
||||||
if n > 10000:
|
|
||||||
return # Just in case
|
|
||||||
if tItem is None:
|
if tItem is None:
|
||||||
# Technically a bug since treeOrder is built from the
|
# Technically a bug since treeOrder is built from the
|
||||||
# same data as projTree
|
# same data as projTree
|
||||||
@@ -1154,10 +1145,11 @@ class NWProject():
|
|||||||
yield tItem
|
yield tItem
|
||||||
elif tItem.itemParent in iterItems:
|
elif tItem.itemParent in iterItems:
|
||||||
# Item's parent exists, but hasn't been sent yet, so add
|
# Item's parent exists, but hasn't been sent yet, so add
|
||||||
# it again to the end
|
# it again to the end, but make sure this doesn't get
|
||||||
|
# out hand, so we cap at 10000 items
|
||||||
logger.warning("Item %s found before its parent" % tHandle)
|
logger.warning("Item %s found before its parent" % tHandle)
|
||||||
iterItems.append(tHandle)
|
iterItems.append(tHandle)
|
||||||
nMax = len(iterItems)
|
nMax = min(len(iterItems), 10000)
|
||||||
else:
|
else:
|
||||||
# Item is orphaned
|
# Item is orphaned
|
||||||
logger.error("Item %s has no parent in current tree" % tHandle)
|
logger.error("Item %s has no parent in current tree" % tHandle)
|
||||||
@@ -1196,13 +1188,12 @@ class NWProject():
|
|||||||
if not os.path.isfile(lockFile):
|
if not os.path.isfile(lockFile):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
theLines = []
|
||||||
try:
|
try:
|
||||||
with open(lockFile, mode="r", encoding="utf8") as inFile:
|
with open(lockFile, mode="r", encoding="utf8") as inFile:
|
||||||
theData = inFile.read()
|
theData = inFile.read()
|
||||||
theLines = theData.splitlines()
|
theLines = theData.splitlines()
|
||||||
if len(theLines) == 4:
|
if len(theLines) != 4:
|
||||||
return theLines
|
|
||||||
else:
|
|
||||||
return ["ERROR"]
|
return ["ERROR"]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1210,7 +1201,7 @@ class NWProject():
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return ["ERROR"]
|
return ["ERROR"]
|
||||||
|
|
||||||
return ["ERROR"]
|
return theLines
|
||||||
|
|
||||||
def _writeLockFile(self):
|
def _writeLockFile(self):
|
||||||
"""Writes a lock file to the project folder.
|
"""Writes a lock file to the project folder.
|
||||||
@@ -1243,13 +1234,12 @@ class NWProject():
|
|||||||
if os.path.isfile(lockFile):
|
if os.path.isfile(lockFile):
|
||||||
try:
|
try:
|
||||||
os.unlink(lockFile)
|
os.unlink(lockFile)
|
||||||
return True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to remove project lockfile")
|
logger.error("Failed to remove project lockfile")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return None
|
return True
|
||||||
|
|
||||||
def _checkFolder(self, thePath):
|
def _checkFolder(self, thePath):
|
||||||
"""Check if a folder exists, and if it doesn't, create it.
|
"""Check if a folder exists, and if it doesn't, create it.
|
||||||
@@ -1292,7 +1282,7 @@ class NWProject():
|
|||||||
back into the project tree.
|
back into the project tree.
|
||||||
"""
|
"""
|
||||||
if self.projPath is None:
|
if self.projPath is None:
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Then check the files in the data folder
|
# Then check the files in the data folder
|
||||||
logger.debug("Checking files in project content folder")
|
logger.debug("Checking files in project content folder")
|
||||||
@@ -1352,7 +1342,7 @@ class NWProject():
|
|||||||
orphItem.setLayout(oLayout)
|
orphItem.setLayout(oLayout)
|
||||||
self.projTree.append(oHandle, None, orphItem)
|
self.projTree.append(oHandle, None, orphItem)
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
def _appendSessionStats(self):
|
def _appendSessionStats(self):
|
||||||
"""Append session statistics to the sessions log file.
|
"""Append session statistics to the sessions log file.
|
||||||
@@ -1363,21 +1353,27 @@ class NWProject():
|
|||||||
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
|
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
|
||||||
isFile = os.path.isfile(sessionFile)
|
isFile = os.path.isfile(sessionFile)
|
||||||
|
|
||||||
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
|
try:
|
||||||
if not isFile:
|
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
|
||||||
# It's a new file, so add a header
|
if not isFile:
|
||||||
if self.lastWCount > 0:
|
# It's a new file, so add a header
|
||||||
outFile.write("# Offset %d\n" % self.lastWCount)
|
if self.lastWCount > 0:
|
||||||
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
outFile.write("# Offset %d\n" % self.lastWCount)
|
||||||
"Start Time", "End Time", "Novel", "Notes"
|
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
||||||
|
"Start Time", "End Time", "Novel", "Notes"
|
||||||
|
))
|
||||||
|
|
||||||
|
outFile.write("%-19s %-19s %8d %8d\n" % (
|
||||||
|
formatTimeStamp(self.projOpened),
|
||||||
|
formatTimeStamp(time()),
|
||||||
|
self.novelWCount,
|
||||||
|
self.notesWCount,
|
||||||
))
|
))
|
||||||
|
|
||||||
outFile.write("%-19s %-19s %8d %8d\n" % (
|
except Exception as e:
|
||||||
formatTimeStamp(self.projOpened),
|
logger.error("Failed to write session stats file")
|
||||||
formatTimeStamp(time()),
|
logger.error(str(e))
|
||||||
self.novelWCount,
|
return False
|
||||||
self.notesWCount,
|
|
||||||
))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1490,7 +1486,8 @@ class NWProject():
|
|||||||
os.unlink(rmFile)
|
os.unlink(rmFile)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
# END Class NWProject
|
# END Class NWProject
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
import nw
|
import nw
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import difflib
|
||||||
from difflib import get_close_matches
|
|
||||||
|
|
||||||
from nw.constants import nwConst, isoLanguage
|
from nw.constants import nwConst, isoLanguage
|
||||||
|
|
||||||
@@ -70,14 +69,16 @@ class NWSpellCheck():
|
|||||||
"""
|
"""
|
||||||
if self.projectDict is not None and newWord not in self.projDict:
|
if self.projectDict is not None and newWord not in self.projDict:
|
||||||
newWord = newWord.strip()
|
newWord = newWord.strip()
|
||||||
self.projDict.append(newWord)
|
|
||||||
try:
|
try:
|
||||||
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
|
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
|
||||||
outFile.write("%s\n" % newWord)
|
outFile.write("%s\n" % newWord)
|
||||||
|
self.projDict.append(newWord)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
|
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return
|
return False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def listDictionaries(self):
|
def listDictionaries(self):
|
||||||
"""Dummy function.
|
"""Dummy function.
|
||||||
@@ -109,9 +110,12 @@ class NWSpellCheck():
|
|||||||
"""
|
"""
|
||||||
self.projDict = []
|
self.projDict = []
|
||||||
if projectDict is not None:
|
if projectDict is not None:
|
||||||
self.projectDict = projectDict
|
|
||||||
if not os.path.isfile(projectDict):
|
if not os.path.isfile(projectDict):
|
||||||
return
|
self.projectDict = None
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.projectDict = projectDict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug("Loading project word list")
|
logger.debug("Loading project word list")
|
||||||
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
|
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
|
||||||
@@ -123,7 +127,9 @@ class NWSpellCheck():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to load project word list")
|
logger.error("Failed to load project word list")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# END Class NWSpellCheck
|
# END Class NWSpellCheck
|
||||||
|
|
||||||
@@ -287,7 +293,7 @@ class NWSpellSimple(NWSpellCheck):
|
|||||||
if len(theWord) == 0:
|
if len(theWord) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
|
theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
|
||||||
theOptions = []
|
theOptions = []
|
||||||
for aWord in theMatches:
|
for aWord in theMatches:
|
||||||
if len(aWord) == 0:
|
if len(aWord) == 0:
|
||||||
@@ -314,14 +320,12 @@ class NWSpellSimple(NWSpellCheck):
|
|||||||
retList = []
|
retList = []
|
||||||
for dictFile in os.listdir(self.mainConf.dictPath):
|
for dictFile in os.listdir(self.mainConf.dictPath):
|
||||||
|
|
||||||
theBits = os.path.splitext(dictFile)
|
fRoot, fExt = os.path.splitext(dictFile)
|
||||||
if len(theBits) != 2:
|
if fExt != ".dict":
|
||||||
continue
|
|
||||||
if theBits[1] != ".dict":
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
spName = "%s [%s]" % (self.expandLanguage(theBits[0]), nwConst.SP_INTERNAL)
|
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
|
||||||
retList.append((theBits[0], spName))
|
retList.append((fRoot, spName))
|
||||||
|
|
||||||
return retList
|
return retList
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class NWStatus():
|
|||||||
theStatus = checkInt(theStatus, 0, False)
|
theStatus = checkInt(theStatus, 0, False)
|
||||||
if theStatus >= 0 and theStatus < self._theLength:
|
if theStatus >= 0 and theStatus < self._theLength:
|
||||||
return self._theLabels[theStatus]
|
return self._theLabels[theStatus]
|
||||||
|
return self._theLabels[0]
|
||||||
|
|
||||||
def setNewEntries(self, newList):
|
def setNewEntries(self, newList):
|
||||||
"""Update the list of entries after they have been modified by
|
"""Update the list of entries after they have been modified by
|
||||||
@@ -108,7 +109,8 @@ class NWStatus():
|
|||||||
return
|
return
|
||||||
|
|
||||||
def countEntry(self, theLabel):
|
def countEntry(self, theLabel):
|
||||||
"""Lookup the usage count of a given entry.
|
"""Increment the counter for a given label. This should be used
|
||||||
|
together with resetCounts in a loop over project items.
|
||||||
"""
|
"""
|
||||||
theIndex = self.lookupEntry(theLabel)
|
theIndex = self.lookupEntry(theLabel)
|
||||||
if theIndex is not None:
|
if theIndex is not None:
|
||||||
@@ -136,18 +138,9 @@ class NWStatus():
|
|||||||
|
|
||||||
for xChild in xParent:
|
for xChild in xParent:
|
||||||
theLabels.append(xChild.text)
|
theLabels.append(xChild.text)
|
||||||
if "red" in xChild.attrib:
|
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
|
||||||
cR = checkInt(xChild.attrib["red"], 0, False)
|
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
|
||||||
else:
|
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
|
||||||
cR = 0
|
|
||||||
if "green" in xChild.attrib:
|
|
||||||
cG = checkInt(xChild.attrib["green"], 0, False)
|
|
||||||
else:
|
|
||||||
cG = 0
|
|
||||||
if "blue" in xChild.attrib:
|
|
||||||
cB = checkInt(xChild.attrib["blue"], 0, False)
|
|
||||||
else:
|
|
||||||
cB = 0
|
|
||||||
theColours.append((cR, cG, cB))
|
theColours.append((cR, cG, cB))
|
||||||
|
|
||||||
if len(theLabels) > 0:
|
if len(theLabels) > 0:
|
||||||
|
|||||||
@@ -68,16 +68,16 @@ class ToHtml(Tokenizer):
|
|||||||
# Setters
|
# Setters
|
||||||
##
|
##
|
||||||
|
|
||||||
def setPreview(self, forPreview, doComments, doSynopsis):
|
def setPreview(self, doComments, doSynopsis):
|
||||||
"""If we're using this class to generate markdown preview, we
|
"""If we're using this class to generate markdown preview, we
|
||||||
need to make a few changes to formatting, which is managed by
|
need to make a few changes to formatting, which is managed by
|
||||||
these flags.
|
these flags.
|
||||||
"""
|
"""
|
||||||
if forPreview:
|
self.genMode = self.M_PREVIEW
|
||||||
self.genMode = self.M_PREVIEW
|
self.doKeywords = True
|
||||||
self.doKeywords = True
|
self.doComments = doComments
|
||||||
self.doComments = doComments
|
self.doSynopsis = doSynopsis
|
||||||
self.doSynopsis = doSynopsis
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def setStyles(self, cssStyles):
|
def setStyles(self, cssStyles):
|
||||||
@@ -141,11 +141,13 @@ class ToHtml(Tokenizer):
|
|||||||
# For novel files for export, we bump the titles one level
|
# For novel files for export, we bump the titles one level
|
||||||
# up as this is more useful for printing and word processor
|
# up as this is more useful for printing and word processor
|
||||||
# imports.
|
# imports.
|
||||||
h1 = "h1 class='title'"
|
h1Cl = " class='title'"
|
||||||
|
h1 = "h1"
|
||||||
h2 = "h1"
|
h2 = "h1"
|
||||||
h3 = "h2"
|
h3 = "h2"
|
||||||
h4 = "h3"
|
h4 = "h3"
|
||||||
else:
|
else:
|
||||||
|
h1Cl = ""
|
||||||
h1 = "h1"
|
h1 = "h1"
|
||||||
h2 = "h2"
|
h2 = "h2"
|
||||||
h3 = "h3"
|
h3 = "h3"
|
||||||
@@ -193,7 +195,7 @@ class ToHtml(Tokenizer):
|
|||||||
else:
|
else:
|
||||||
aNm = ""
|
aNm = ""
|
||||||
|
|
||||||
# Process TextType
|
# Process Text Type
|
||||||
if tType == self.T_EMPTY:
|
if tType == self.T_EMPTY:
|
||||||
if parStyle is None:
|
if parStyle is None:
|
||||||
parStyle = ""
|
parStyle = ""
|
||||||
@@ -214,7 +216,7 @@ class ToHtml(Tokenizer):
|
|||||||
|
|
||||||
elif tType == self.T_HEAD1:
|
elif tType == self.T_HEAD1:
|
||||||
tHead = tText.replace(r"\\", "<br/>")
|
tHead = tText.replace(r"\\", "<br/>")
|
||||||
tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
|
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
|
||||||
|
|
||||||
elif tType == self.T_HEAD2:
|
elif tType == self.T_HEAD2:
|
||||||
tHead = tText.replace(r"\\", "<br/>")
|
tHead = tText.replace(r"\\", "<br/>")
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class Tokenizer():
|
|||||||
##
|
##
|
||||||
|
|
||||||
def addRootHeading(self, theHandle):
|
def addRootHeading(self, theHandle):
|
||||||
"""Add a heading at the start if a new root folder.
|
"""Add a heading at the start of a new root folder.
|
||||||
"""
|
"""
|
||||||
theItem = self.theProject.projTree[theHandle]
|
theItem = self.theProject.projTree[theHandle]
|
||||||
if theItem is None:
|
if theItem is None:
|
||||||
@@ -205,7 +205,7 @@ class Tokenizer():
|
|||||||
self.theHandle = theHandle
|
self.theHandle = theHandle
|
||||||
self.theItem = self.theProject.projTree[theHandle]
|
self.theItem = self.theProject.projTree[theHandle]
|
||||||
if self.theItem is None:
|
if self.theItem is None:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if theText is not None:
|
if theText is not None:
|
||||||
# If the text is set, just use that
|
# If the text is set, just use that
|
||||||
@@ -234,7 +234,7 @@ class Tokenizer():
|
|||||||
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
|
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
|
||||||
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
|
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self):
|
||||||
"""Return the result from the conversion.
|
"""Return the result from the conversion.
|
||||||
@@ -244,6 +244,8 @@ class Tokenizer():
|
|||||||
def getResultSize(self):
|
def getResultSize(self):
|
||||||
"""Return the size of the result from the conversion.
|
"""Return the size of the result from the conversion.
|
||||||
"""
|
"""
|
||||||
|
if self.theResult is None:
|
||||||
|
return 0
|
||||||
return len(self.theResult)
|
return len(self.theResult)
|
||||||
|
|
||||||
def getFilteredMarkdown(self):
|
def getFilteredMarkdown(self):
|
||||||
@@ -445,7 +447,7 @@ class Tokenizer():
|
|||||||
"""
|
"""
|
||||||
# No special header formatting for notes and no-layout files
|
# No special header formatting for notes and no-layout files
|
||||||
if self.isNone or self.isNote:
|
if self.isNone or self.isNote:
|
||||||
return
|
return False
|
||||||
|
|
||||||
# For novel files, we need to handle chapter numbering, scene
|
# For novel files, we need to handle chapter numbering, scene
|
||||||
# numbering, and scene breaks
|
# numbering, and scene breaks
|
||||||
@@ -479,8 +481,7 @@ class Tokenizer():
|
|||||||
if self.isUnNum:
|
if self.isUnNum:
|
||||||
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
|
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
|
||||||
elif tToken[2].startswith("*"):
|
elif tToken[2].startswith("*"):
|
||||||
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
|
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
|
||||||
tTemp = tTemp[1:].lstrip()
|
|
||||||
else:
|
else:
|
||||||
self.numChapter += 1
|
self.numChapter += 1
|
||||||
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
|
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
|
||||||
@@ -663,7 +664,7 @@ class Tokenizer():
|
|||||||
self.A_LEFT
|
self.A_LEFT
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
|
|||||||
@@ -141,6 +141,15 @@ def _numberToWordEN(numVal):
|
|||||||
tenWord = ""
|
tenWord = ""
|
||||||
hunWord = ""
|
hunWord = ""
|
||||||
|
|
||||||
|
if not isinstance(numVal, int):
|
||||||
|
return "[NaN]"
|
||||||
|
|
||||||
|
if numVal < 0:
|
||||||
|
return "[Negative]"
|
||||||
|
|
||||||
|
if numVal > 999:
|
||||||
|
return "[Out of Range]"
|
||||||
|
|
||||||
if numVal == 0:
|
if numVal == 0:
|
||||||
return "Zero"
|
return "Zero"
|
||||||
|
|
||||||
@@ -166,19 +175,20 @@ def _numberToWordEN(numVal):
|
|||||||
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
|
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retVale = ""
|
||||||
hunWord = theHundreds.get(hunVal, "")
|
hunWord = theHundreds.get(hunVal, "")
|
||||||
if tenVal == 10:
|
if tenVal == 10:
|
||||||
oneWord = theTeens.get(oneVal, "")
|
oneWord = theTeens.get(oneVal, "")
|
||||||
return f"{hunWord} {oneWord}".strip()
|
retVale = f"{hunWord} {oneWord}".strip()
|
||||||
else:
|
else:
|
||||||
oneWord = theOnes.get(oneVal, "")
|
oneWord = theOnes.get(oneVal, "")
|
||||||
if tenVal == 0:
|
if tenVal == 0:
|
||||||
return f"{hunWord} {oneWord}".strip()
|
retVale = f"{hunWord} {oneWord}".strip()
|
||||||
else:
|
else:
|
||||||
tenWord = theTens.get(tenVal, "")
|
tenWord = theTens.get(tenVal, "")
|
||||||
if oneVal == 0:
|
if oneVal == 0:
|
||||||
return f"{hunWord} {tenWord}".strip()
|
retVale = f"{hunWord} {tenWord}".strip()
|
||||||
else:
|
else:
|
||||||
return f"{hunWord} {tenWord}-{oneWord}".strip()
|
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
|
||||||
|
|
||||||
return ""
|
return retVale
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ class NWTree():
|
|||||||
self._treeOrder = [] # The order of the tree items on the tree view
|
self._treeOrder = [] # The order of the tree items on the tree view
|
||||||
self._treeRoots = [] # The root items of the tree
|
self._treeRoots = [] # The root items of the tree
|
||||||
self._trashRoot = None # The handle of the trash root folder
|
self._trashRoot = None # The handle of the trash root folder
|
||||||
|
self._archRoot = None # The handle of the archive root folder
|
||||||
self._theIndex = 0 # The current iterator index
|
self._theIndex = 0 # The current iterator index
|
||||||
self._treeChanged = False # True if tree structure has changed
|
self._treeChanged = False # True if tree structure has changed
|
||||||
|
|
||||||
self._handleSeed = None # Used for generating handles for testing
|
self._handleSeed = None # Used for generating handles for testing
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -83,14 +85,15 @@ class NWTree():
|
|||||||
if tHandle is None:
|
if tHandle is None:
|
||||||
tHandle = self._makeHandle()
|
tHandle = self._makeHandle()
|
||||||
|
|
||||||
|
if tHandle in self._projTree:
|
||||||
|
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
|
||||||
|
return False
|
||||||
|
|
||||||
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
|
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
|
||||||
|
|
||||||
nwItem.setHandle(tHandle)
|
nwItem.setHandle(tHandle)
|
||||||
nwItem.setParent(pHandle)
|
nwItem.setParent(pHandle)
|
||||||
|
|
||||||
self._projTree[tHandle] = nwItem
|
|
||||||
self._treeOrder.append(tHandle)
|
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.ROOT:
|
if nwItem.itemType == nwItemType.ROOT:
|
||||||
logger.verbose("Item %s is a root item" % str(tHandle))
|
logger.verbose("Item %s is a root item" % str(tHandle))
|
||||||
self._treeRoots.append(tHandle)
|
self._treeRoots.append(tHandle)
|
||||||
@@ -104,10 +107,13 @@ class NWTree():
|
|||||||
self._trashRoot = tHandle
|
self._trashRoot = tHandle
|
||||||
else:
|
else:
|
||||||
logger.error("Only one trash folder allowed")
|
logger.error("Only one trash folder allowed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._projTree[tHandle] = nwItem
|
||||||
|
self._treeOrder.append(tHandle)
|
||||||
self._setTreeChanged(True)
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
def packXML(self, xParent):
|
def packXML(self, xParent):
|
||||||
"""Pack the content of the tree into the provided XML object. In
|
"""Pack the content of the tree into the provided XML object. In
|
||||||
@@ -176,8 +182,9 @@ class NWTree():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
return
|
return True
|
||||||
|
|
||||||
def sumWords(self):
|
def sumWords(self):
|
||||||
"""Loops over all entries and adds up the word counts.
|
"""Loops over all entries and adds up the word counts.
|
||||||
@@ -244,6 +251,8 @@ class NWTree():
|
|||||||
return True
|
return True
|
||||||
for aRoot in self._treeRoots:
|
for aRoot in self._treeRoots:
|
||||||
tItem = self.__getitem__(aRoot)
|
tItem = self.__getitem__(aRoot)
|
||||||
|
if tItem is None:
|
||||||
|
continue
|
||||||
if theClass == tItem.itemClass:
|
if theClass == tItem.itemClass:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -397,7 +406,7 @@ class NWTree():
|
|||||||
del self._projTree[tHandle]
|
del self._projTree[tHandle]
|
||||||
else:
|
else:
|
||||||
logger.warning("Failed to delete item %s: item not found" % tHandle)
|
logger.warning("Failed to delete item %s: item not found" % tHandle)
|
||||||
return False
|
return
|
||||||
|
|
||||||
if tHandle in self._treeRoots:
|
if tHandle in self._treeRoots:
|
||||||
self._treeRoots.remove(tHandle)
|
self._treeRoots.remove(tHandle)
|
||||||
@@ -408,7 +417,7 @@ class NWTree():
|
|||||||
|
|
||||||
self._setTreeChanged(True)
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
return True
|
return
|
||||||
|
|
||||||
def __contains__(self, tHandle):
|
def __contains__(self, tHandle):
|
||||||
"""Checks if a handle exists in the tree.
|
"""Checks if a handle exists in the tree.
|
||||||
|
|||||||
@@ -135,10 +135,9 @@ class NWErrorMessage(QDialog):
|
|||||||
# END Class NWErrorMessage
|
# END Class NWErrorMessage
|
||||||
|
|
||||||
|
|
||||||
def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
def exceptionHandler(exType, exValue, exTrace):
|
||||||
"""Function to catch unhandled global exceptions.
|
"""Function to catch unhandled global exceptions.
|
||||||
"""
|
"""
|
||||||
import nw
|
|
||||||
import logging
|
import logging
|
||||||
from traceback import print_tb
|
from traceback import print_tb
|
||||||
from PyQt5.QtWidgets import qApp
|
from PyQt5.QtWidgets import qApp
|
||||||
@@ -160,8 +159,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
|||||||
|
|
||||||
errMsg = NWErrorMessage(nwGUI)
|
errMsg = NWErrorMessage(nwGUI)
|
||||||
errMsg.setMessage(exType, exValue, exTrace)
|
errMsg.setMessage(exType, exValue, exTrace)
|
||||||
if nw.CONFIG.showGUI:
|
errMsg.exec_()
|
||||||
errMsg.exec_()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try a controlled shudown
|
# Try a controlled shudown
|
||||||
@@ -173,10 +171,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
|||||||
logger.critical("Could not close the project before exiting")
|
logger.critical("Could not close the project before exiting")
|
||||||
logger.critical(str(e))
|
logger.critical(str(e))
|
||||||
|
|
||||||
if testMode:
|
qApp.exit(1)
|
||||||
return errMsg.msgBody.toPlainText()
|
|
||||||
else:
|
|
||||||
qApp.exit(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(str(e))
|
logger.critical(str(e))
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ import os
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QCursor
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
|
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
|
||||||
QTextBrowser, QLabel
|
QTextBrowser, QLabel
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,6 +81,10 @@ class GuiAbout(QDialog):
|
|||||||
self.pageAbout.setOpenExternalLinks(True)
|
self.pageAbout.setOpenExternalLinks(True)
|
||||||
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
|
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||||
|
|
||||||
|
self.pageNotes = QTextBrowser()
|
||||||
|
self.pageNotes.setOpenExternalLinks(True)
|
||||||
|
self.pageNotes.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||||
|
|
||||||
self.pageLicense = QTextBrowser()
|
self.pageLicense = QTextBrowser()
|
||||||
self.pageLicense.setOpenExternalLinks(True)
|
self.pageLicense.setOpenExternalLinks(True)
|
||||||
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
|
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||||
@@ -87,6 +92,7 @@ class GuiAbout(QDialog):
|
|||||||
# Main Tab Area
|
# Main Tab Area
|
||||||
self.tabBox = QTabWidget()
|
self.tabBox = QTabWidget()
|
||||||
self.tabBox.addTab(self.pageAbout, "About")
|
self.tabBox.addTab(self.pageAbout, "About")
|
||||||
|
self.tabBox.addTab(self.pageNotes, "Release")
|
||||||
self.tabBox.addTab(self.pageLicense, "License")
|
self.tabBox.addTab(self.pageLicense, "License")
|
||||||
self.innerBox.addWidget(self.tabBox)
|
self.innerBox.addWidget(self.tabBox)
|
||||||
|
|
||||||
@@ -98,14 +104,27 @@ class GuiAbout(QDialog):
|
|||||||
self.outerBox.addWidget(self.buttonBox)
|
self.outerBox.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
self._setStyleSheet()
|
|
||||||
self._fillAboutPage()
|
|
||||||
self._fillLicensePage()
|
|
||||||
|
|
||||||
logger.debug("GuiAbout initialisation complete")
|
logger.debug("GuiAbout initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def populateGUI(self):
|
||||||
|
"""Populate tabs with text.
|
||||||
|
"""
|
||||||
|
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||||
|
self._setStyleSheet()
|
||||||
|
self._fillAboutPage()
|
||||||
|
self._fillNotesPage()
|
||||||
|
self._fillLicensePage()
|
||||||
|
qApp.restoreOverrideCursor()
|
||||||
|
return
|
||||||
|
|
||||||
|
def showReleaseNotes(self):
|
||||||
|
"""Show the release notes.
|
||||||
|
"""
|
||||||
|
self.tabBox.setCurrentWidget(self.pageNotes)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
@@ -121,14 +140,14 @@ class GuiAbout(QDialog):
|
|||||||
"<p>novelWriter is a markdown-like text editor designed for "
|
"<p>novelWriter is a markdown-like text editor designed for "
|
||||||
"organising and writing novels. It is written in Python 3 with a "
|
"organising and writing novels. It is written in Python 3 with a "
|
||||||
"Qt5 GUI, using PyQt5.</p>"
|
"Qt5 GUI, using PyQt5.</p>"
|
||||||
"<p>novelWriter is free software: you can redistribute it and/or "
|
"<p>novelWriter is free software: you can redistribute it and "
|
||||||
"modify it under the terms of the GNU General Public License as "
|
"modify it under the terms of the GNU General Public License as "
|
||||||
"published by the Free Software Foundation, either version 3 of "
|
"published by the Free Software Foundation, either version 3 of "
|
||||||
"the License, or (at your option) any later version.</p>"
|
"the License, or (at your option) any later version.</p>"
|
||||||
"<p>novelWriter is distributed in the hope that it will be useful, "
|
"<p>novelWriter is distributed in the hope that it will be useful, "
|
||||||
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
|
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
|
||||||
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
|
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
|
||||||
"<p>See the License tab for the full text, or visit the GNU website "
|
"<p>See the License tab for the full license text, or visit the GNU website "
|
||||||
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
|
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
|
||||||
"for more details.</p>"
|
"for more details.</p>"
|
||||||
"<h3>Credits</h3>"
|
"<h3>Credits</h3>"
|
||||||
@@ -192,11 +211,22 @@ class GuiAbout(QDialog):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _fillNotesPage(self):
|
||||||
|
"""Load the content for the Release Notes page.
|
||||||
|
"""
|
||||||
|
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
|
||||||
|
if os.path.isfile(docPath):
|
||||||
|
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||||
|
helpText = inFile.read()
|
||||||
|
self.pageNotes.setHtml(helpText)
|
||||||
|
else:
|
||||||
|
self.pageNotes.setHtml("Error loading release notes text ...")
|
||||||
|
return
|
||||||
|
|
||||||
def _fillLicensePage(self):
|
def _fillLicensePage(self):
|
||||||
"""Load the content for the License page.
|
"""Load the content for the License page.
|
||||||
"""
|
"""
|
||||||
docName = "gplv3_%s.htm" % self.mainConf.guiLang
|
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
|
||||||
docPath = os.path.join(self.mainConf.assetPath, "text", docName)
|
|
||||||
if os.path.isfile(docPath):
|
if os.path.isfile(docPath):
|
||||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||||
helpText = inFile.read()
|
helpText = inFile.read()
|
||||||
@@ -221,6 +251,7 @@ class GuiAbout(QDialog):
|
|||||||
hColB = self.theParent.theTheme.colHead[2],
|
hColB = self.theParent.theTheme.colHead[2],
|
||||||
)
|
)
|
||||||
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
||||||
|
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
|
||||||
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
|
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -262,10 +262,10 @@ class GuiDocEditor(QTextEdit):
|
|||||||
# If we have a document open, we should reload it in case the
|
# If we have a document open, we should reload it in case the
|
||||||
# font changed, otherwise we just clear the editor entirely,
|
# font changed, otherwise we just clear the editor entirely,
|
||||||
# which makes it read only.
|
# which makes it read only.
|
||||||
if self.theHandle is not None:
|
if self.theHandle is None:
|
||||||
self.redrawText()
|
|
||||||
else:
|
|
||||||
self.clearEditor()
|
self.clearEditor()
|
||||||
|
else:
|
||||||
|
self.redrawText()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -355,6 +355,7 @@ class GuiDocEditor(QTextEdit):
|
|||||||
"""Redraw the text by marking the document content as "dirty".
|
"""Redraw the text by marking the document content as "dirty".
|
||||||
"""
|
"""
|
||||||
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
||||||
|
self.updateDocMargins()
|
||||||
return
|
return
|
||||||
|
|
||||||
def replaceText(self, theText):
|
def replaceText(self, theText):
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
|
|
||||||
# If we have a document open, we should reload it in case the font changed
|
# If we have a document open, we should reload it in case the font changed
|
||||||
if self.theHandle is not None:
|
if self.theHandle is not None:
|
||||||
self.redrawText()
|
self.reloadText()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
|
|
||||||
sPos = self.verticalScrollBar().value()
|
sPos = self.verticalScrollBar().value()
|
||||||
aDoc = ToHtml(self.theProject, self.theParent)
|
aDoc = ToHtml(self.theProject, self.theParent)
|
||||||
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
|
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
|
||||||
aDoc.setLinkHeaders(True)
|
aDoc.setLinkHeaders(True)
|
||||||
|
|
||||||
# Be extra careful here to prevent crashes when first opening a
|
# Be extra careful here to prevent crashes when first opening a
|
||||||
@@ -233,6 +233,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
"""Redraw the text by marking the document content as "dirty".
|
"""Redraw the text by marking the document content as "dirty".
|
||||||
"""
|
"""
|
||||||
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
||||||
|
self.updateDocMargins()
|
||||||
return
|
return
|
||||||
|
|
||||||
def loadFromTag(self, theTag):
|
def loadFromTag(self, theTag):
|
||||||
|
|||||||
@@ -598,8 +598,8 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# Insert > Separator
|
# Insert > Separator
|
||||||
self.insertMenu.addSeparator()
|
self.insertMenu.addSeparator()
|
||||||
|
|
||||||
# Insert > Keywords and Tags
|
# Insert > Tags and References
|
||||||
self.mInsKeywords = self.insertMenu.addMenu("Keywords and Tags")
|
self.mInsKeywords = self.insertMenu.addMenu("Tags and References")
|
||||||
self.mInsKWItems = {}
|
self.mInsKWItems = {}
|
||||||
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
|
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
|
||||||
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
|
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
|
||||||
@@ -908,7 +908,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.helpMenu.addAction(self.aHelpLoc)
|
self.helpMenu.addAction(self.aHelpLoc)
|
||||||
|
|
||||||
self.aHelpWeb = QAction("Documentation (Online)", self)
|
self.aHelpWeb = QAction("Documentation (Online)", self)
|
||||||
self.aHelpWeb.setStatusTip("View online documentation")
|
self.aHelpWeb.setStatusTip("View online documentation at %s" % nw.__docurl__)
|
||||||
self.aHelpWeb.triggered.connect(lambda: self._openWebsite(nw.__docurl__))
|
self.aHelpWeb.triggered.connect(lambda: self._openWebsite(nw.__docurl__))
|
||||||
if self.mainConf.hasHelp and self.mainConf.hasAssistant:
|
if self.mainConf.hasHelp and self.mainConf.hasAssistant:
|
||||||
self.aHelpWeb.setShortcut("Shift+F1")
|
self.aHelpWeb.setShortcut("Shift+F1")
|
||||||
@@ -916,18 +916,33 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aHelpWeb.setShortcuts(["F1", "Shift+F1"])
|
self.aHelpWeb.setShortcuts(["F1", "Shift+F1"])
|
||||||
self.helpMenu.addAction(self.aHelpWeb)
|
self.helpMenu.addAction(self.aHelpWeb)
|
||||||
|
|
||||||
# Document > Go to Website
|
# Help > Separator
|
||||||
self.aWebsite = QAction("Open the novelWriter Website", self)
|
self.helpMenu.addSeparator()
|
||||||
self.aWebsite.setStatusTip("View the main website")
|
|
||||||
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
|
|
||||||
self.helpMenu.addAction(self.aWebsite)
|
|
||||||
|
|
||||||
# Document > Report Issue
|
# Document > Report an Issue
|
||||||
self.aIssue = QAction("Report an Issue", self)
|
self.aIssue = QAction("Report an Issue (GitHub)", self)
|
||||||
self.aIssue.setStatusTip("Report a bug or issue on GitHub")
|
self.aIssue.setStatusTip("Report a bug or issue on GitHub at %s" % nw.__issuesurl__)
|
||||||
self.aIssue.triggered.connect(lambda: self._openWebsite(nw.__issuesurl__))
|
self.aIssue.triggered.connect(lambda: self._openWebsite(nw.__issuesurl__))
|
||||||
self.helpMenu.addAction(self.aIssue)
|
self.helpMenu.addAction(self.aIssue)
|
||||||
|
|
||||||
|
# Document > Ask a Question
|
||||||
|
self.aQuestion = QAction("Ask a Question (GitHub)", self)
|
||||||
|
self.aQuestion.setStatusTip("Ask a question on GitHub at %s" % nw.__helpurl__)
|
||||||
|
self.aQuestion.triggered.connect(lambda: self._openWebsite(nw.__helpurl__))
|
||||||
|
self.helpMenu.addAction(self.aQuestion)
|
||||||
|
|
||||||
|
# Document > Latest Release
|
||||||
|
self.aRelease = QAction("Latest Release (GitHub)", self)
|
||||||
|
self.aRelease.setStatusTip("Open the Releases page on GitHub at %s" % nw.__releaseurl__)
|
||||||
|
self.aRelease.triggered.connect(lambda: self._openWebsite(nw.__releaseurl__))
|
||||||
|
self.helpMenu.addAction(self.aRelease)
|
||||||
|
|
||||||
|
# Document > Main Website
|
||||||
|
self.aWebsite = QAction("The novelWriter Website", self)
|
||||||
|
self.aWebsite.setStatusTip("Open the novelWriter website at %s" % nw.__url__)
|
||||||
|
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
|
||||||
|
self.helpMenu.addAction(self.aWebsite)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class GuiMainMenu
|
# END Class GuiMainMenu
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Show full path in document header",
|
"Show full path in document header",
|
||||||
self.showFullPath,
|
self.showFullPath,
|
||||||
"Shows the document title and parent folder names."
|
"Add the parent folder names to the header."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hideVScroll = QSwitch()
|
self.hideVScroll = QSwitch()
|
||||||
@@ -235,7 +235,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Hide vertical scroll bars in main windows",
|
"Hide vertical scroll bars in main windows",
|
||||||
self.hideVScroll,
|
self.hideVScroll,
|
||||||
"Scrolling with mouse wheel and keys only."
|
"Scrolling available with mouse wheel and keys only."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hideHScroll = QSwitch()
|
self.hideHScroll = QSwitch()
|
||||||
@@ -243,7 +243,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Hide horizontal scroll bars in main windows",
|
"Hide horizontal scroll bars in main windows",
|
||||||
self.hideHScroll,
|
self.hideHScroll,
|
||||||
"Scrolling with mouse wheel and keys only."
|
"Scrolling available with mouse wheel and keys only."
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -325,8 +325,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
|||||||
self.autoSaveDoc.setSingleStep(1)
|
self.autoSaveDoc.setSingleStep(1)
|
||||||
self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc)
|
self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc)
|
||||||
self.backupPathRow = self.mainForm.addRow(
|
self.backupPathRow = self.mainForm.addRow(
|
||||||
"Save interval for the currently open document",
|
"Save document interval",
|
||||||
self.autoSaveDoc,
|
self.autoSaveDoc,
|
||||||
|
"How often the open document is automatically saved.",
|
||||||
theUnit="seconds"
|
theUnit="seconds"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -337,8 +338,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
|||||||
self.autoSaveProj.setSingleStep(1)
|
self.autoSaveProj.setSingleStep(1)
|
||||||
self.autoSaveProj.setValue(self.mainConf.autoSaveProj)
|
self.autoSaveProj.setValue(self.mainConf.autoSaveProj)
|
||||||
self.backupPathRow = self.mainForm.addRow(
|
self.backupPathRow = self.mainForm.addRow(
|
||||||
"Save interval for the currently open project",
|
"Save project interval",
|
||||||
self.autoSaveProj,
|
self.autoSaveProj,
|
||||||
|
"How often the open project is automatically saved.",
|
||||||
theUnit="seconds"
|
theUnit="seconds"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -361,9 +363,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
|||||||
self.backupOnClose.setChecked(self.mainConf.backupOnClose)
|
self.backupOnClose.setChecked(self.mainConf.backupOnClose)
|
||||||
self.backupOnClose.toggled.connect(self._toggledBackupOnClose)
|
self.backupOnClose.toggled.connect(self._toggledBackupOnClose)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Run backup when closing project",
|
"Run backup when the project is closed",
|
||||||
self.backupOnClose,
|
self.backupOnClose,
|
||||||
"This option can be overridden in project settings."
|
"Can be overridden for individual projects in project settings."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Ask before backup
|
## Ask before backup
|
||||||
@@ -373,7 +375,8 @@ class GuiConfigEditProjectsTab(QWidget):
|
|||||||
self.askBeforeBackup.setEnabled(self.mainConf.backupOnClose)
|
self.askBeforeBackup.setEnabled(self.mainConf.backupOnClose)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Ask before running backup",
|
"Ask before running backup",
|
||||||
self.askBeforeBackup
|
self.askBeforeBackup,
|
||||||
|
"Disabling this will cause backups to run in the background."
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -426,7 +429,7 @@ class GuiConfigEditProjectsTab(QWidget):
|
|||||||
|
|
||||||
def _toggledBackupOnClose(self, theState):
|
def _toggledBackupOnClose(self, theState):
|
||||||
"""Enable or disable switch that depends on the backup on close
|
"""Enable or disable switch that depends on the backup on close
|
||||||
switch,
|
switch.
|
||||||
"""
|
"""
|
||||||
self.askBeforeBackup.setEnabled(theState)
|
self.askBeforeBackup.setEnabled(theState)
|
||||||
return
|
return
|
||||||
@@ -449,7 +452,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
|
|
||||||
# Text Style
|
# Text Style
|
||||||
# ==========
|
# ==========
|
||||||
self.mainForm.addGroupLabel("Text Style")
|
self.mainForm.addGroupLabel("Document Text Style")
|
||||||
|
|
||||||
## Font Family
|
## Font Family
|
||||||
self.textStyleFont = QLineEdit()
|
self.textStyleFont = QLineEdit()
|
||||||
@@ -475,12 +478,13 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Font size",
|
"Font size",
|
||||||
self.textStyleSize,
|
self.textStyleSize,
|
||||||
|
"Font size for the document editor and viewer.",
|
||||||
theUnit = "pt"
|
theUnit = "pt"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Text Flow
|
# Text Flow
|
||||||
# =========
|
# =========
|
||||||
self.mainForm.addGroupLabel("Text Flow")
|
self.mainForm.addGroupLabel("Document Text Flow")
|
||||||
|
|
||||||
## Max Text Width in Normal Mode
|
## Max Text Width in Normal Mode
|
||||||
self.textFlowMax = QSpinBox(self)
|
self.textFlowMax = QSpinBox(self)
|
||||||
@@ -491,6 +495,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Maximum text width in \"Normal Mode\"",
|
"Maximum text width in \"Normal Mode\"",
|
||||||
self.textFlowMax,
|
self.textFlowMax,
|
||||||
|
"Horizontal margins are scaled automatically.",
|
||||||
theUnit="px"
|
theUnit="px"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -503,6 +508,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Maximum text width in \"Focus Mode\"",
|
"Maximum text width in \"Focus Mode\"",
|
||||||
self.focusDocWidth,
|
self.focusDocWidth,
|
||||||
|
"Horizontal margins are scaled automatically.",
|
||||||
theUnit="px"
|
theUnit="px"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -512,7 +518,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Disable maximum text width in \"Normal Mode\"",
|
"Disable maximum text width in \"Normal Mode\"",
|
||||||
self.textFlowFixed,
|
self.textFlowFixed,
|
||||||
"If disabled, minimum text width is defined by the margin setting."
|
"If disabled, minimum text width is defined by the margin."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Focus Mode Footer
|
## Focus Mode Footer
|
||||||
@@ -520,7 +526,8 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.hideFocusFooter.setChecked(self.mainConf.hideFocusFooter)
|
self.hideFocusFooter.setChecked(self.mainConf.hideFocusFooter)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Hide document footer in \"Focus Mode\"",
|
"Hide document footer in \"Focus Mode\"",
|
||||||
self.hideFocusFooter
|
self.hideFocusFooter,
|
||||||
|
"Hide the information bar at the bottom of the document."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Justify Text
|
## Justify Text
|
||||||
@@ -528,7 +535,8 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.textJustify.setChecked(self.mainConf.textFixedW)
|
self.textJustify.setChecked(self.mainConf.textFixedW)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Justify the text margins in editor and viewer",
|
"Justify the text margins in editor and viewer",
|
||||||
self.textJustify
|
self.textJustify,
|
||||||
|
"Lay out text with straight edges in the editor and viewer."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Document Margins
|
## Document Margins
|
||||||
@@ -538,9 +546,9 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.textMargin.setSingleStep(1)
|
self.textMargin.setSingleStep(1)
|
||||||
self.textMargin.setValue(self.mainConf.textMargin)
|
self.textMargin.setValue(self.mainConf.textMargin)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Document text margin",
|
"Text margin",
|
||||||
self.textMargin,
|
self.textMargin,
|
||||||
"If max width is enabled, this is the minimum margin.",
|
"If maximum width is set, this becomes the minimum margin.",
|
||||||
theUnit="px"
|
theUnit="px"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -551,8 +559,9 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.tabWidth.setSingleStep(1)
|
self.tabWidth.setSingleStep(1)
|
||||||
self.tabWidth.setValue(self.mainConf.tabWidth)
|
self.tabWidth.setValue(self.mainConf.tabWidth)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Document tab width",
|
"Tab width",
|
||||||
self.tabWidth,
|
self.tabWidth,
|
||||||
|
"The width of a tab key press in the editor and viewer.",
|
||||||
theUnit="px"
|
theUnit="px"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -566,7 +575,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Scroll past end of the document",
|
"Scroll past end of the document",
|
||||||
self.scrollPastEnd,
|
self.scrollPastEnd,
|
||||||
"Allows scrolling until last line is at the top."
|
"Allow scrolling until the last line is centred in the editor."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Typewriter Scrolling
|
## Typewriter Scrolling
|
||||||
@@ -575,7 +584,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Typewriter style scrolling when you type",
|
"Typewriter style scrolling when you type",
|
||||||
self.autoScroll,
|
self.autoScroll,
|
||||||
"Tries to keep the cursor at a fixed vertical position."
|
"Try to keep the cursor at a fixed vertical position."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Font Size
|
## Font Size
|
||||||
@@ -678,7 +687,7 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Highlight theme",
|
"Highlight theme",
|
||||||
self.selectSyntax,
|
self.selectSyntax,
|
||||||
""
|
"Colour theme to apply to the editor and viewer."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.highlightQuotes = QSwitch()
|
self.highlightQuotes = QSwitch()
|
||||||
@@ -686,7 +695,7 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Highlight text wrapped in quotes",
|
"Highlight text wrapped in quotes",
|
||||||
self.highlightQuotes,
|
self.highlightQuotes,
|
||||||
helpText="Applies to single, double and straight quotes."
|
"Applies to single, double and straight quotes."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.highlightEmph = QSwitch()
|
self.highlightEmph = QSwitch()
|
||||||
@@ -694,7 +703,7 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Add highlight colour to emphasised text",
|
"Add highlight colour to emphasised text",
|
||||||
self.highlightEmph,
|
self.highlightEmph,
|
||||||
helpText="Applies to emphasis, strong and strikethrough."
|
"Applies to emphasis, strong and strikethrough."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Spell Checking
|
# Spell Checking
|
||||||
@@ -724,7 +733,8 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
)
|
)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Spell check language",
|
"Spell check language",
|
||||||
self.spellLangList
|
self.spellLangList,
|
||||||
|
"Available languages are determined by your system."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Big Document Size Limit
|
## Big Document Size Limit
|
||||||
@@ -736,7 +746,7 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Big document limit",
|
"Big document limit",
|
||||||
self.bigDocLimit,
|
self.bigDocLimit,
|
||||||
"Disables full spell checking over the size limit.",
|
"Full spell checking is disabled above this limit.",
|
||||||
theUnit="kB"
|
theUnit="kB"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -749,7 +759,8 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.showTabsNSpaces.setChecked(self.mainConf.showTabsNSpaces)
|
self.showTabsNSpaces.setChecked(self.mainConf.showTabsNSpaces)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Show tabs and spaces",
|
"Show tabs and spaces",
|
||||||
self.showTabsNSpaces
|
self.showTabsNSpaces,
|
||||||
|
"Add symbols to indicate tabs and spaces in the editor."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Show Line Endings
|
## Show Line Endings
|
||||||
@@ -757,7 +768,8 @@ class GuiConfigEditEditingTab(QWidget):
|
|||||||
self.showLineEndings.setChecked(self.mainConf.showLineEndings)
|
self.showLineEndings.setChecked(self.mainConf.showLineEndings)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Show line endings",
|
"Show line endings",
|
||||||
self.showLineEndings
|
self.showLineEndings,
|
||||||
|
"Add a symbol to indicate line endings in the editor."
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -866,7 +878,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Auto-replace text as you type",
|
"Auto-replace text as you type",
|
||||||
self.autoReplaceMain,
|
self.autoReplaceMain,
|
||||||
"Apply formatting to word under cursor if no selection is made."
|
"Allow the editor to replace symbols as you type."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Auto-Replace
|
# Auto-Replace
|
||||||
@@ -880,7 +892,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Auto-replace single quotes",
|
"Auto-replace single quotes",
|
||||||
self.autoReplaceSQ,
|
self.autoReplaceSQ,
|
||||||
"The feature will try to guess opening or closing single quote."
|
"Try to guess which is an opening or a closing single quote."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Auto-Replace Double Quotes
|
## Auto-Replace Double Quotes
|
||||||
@@ -890,7 +902,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Auto-replace double quotes",
|
"Auto-replace double quotes",
|
||||||
self.autoReplaceDQ,
|
self.autoReplaceDQ,
|
||||||
"The feature will try to guess opening or closing quote quote."
|
"Try to guess which is an opening or a closing double quote."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Auto-Replace Hyphens
|
## Auto-Replace Hyphens
|
||||||
@@ -900,7 +912,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Auto-replace dashes",
|
"Auto-replace dashes",
|
||||||
self.autoReplaceDash,
|
self.autoReplaceDash,
|
||||||
"Auto-replace double and triple hyphens with short and long dash."
|
"Double and triple hyphens become short and long dashes."
|
||||||
)
|
)
|
||||||
|
|
||||||
## Auto-Replace Dots
|
## Auto-Replace Dots
|
||||||
@@ -910,7 +922,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Auto-replace dots",
|
"Auto-replace dots",
|
||||||
self.autoReplaceDots,
|
self.autoReplaceDots,
|
||||||
"Auto-replace three dots with ellipsis."
|
"Three consecutive dots becomes ellipsis."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Quotation Style
|
# Quotation Style
|
||||||
@@ -934,7 +946,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Single quote open style",
|
"Single quote open style",
|
||||||
self.quoteSym["SO"],
|
self.quoteSym["SO"],
|
||||||
"Auto-replaces apostrophe before words.",
|
"The symbol to use for a leading single quote.",
|
||||||
theButton=self.btnSingleStyleO
|
theButton=self.btnSingleStyleO
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -950,7 +962,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Single quote close style",
|
"Single quote close style",
|
||||||
self.quoteSym["SC"],
|
self.quoteSym["SC"],
|
||||||
"Auto-replaces apostrophe after words.",
|
"The symbol to use for a trailing single quote.",
|
||||||
theButton=self.btnSingleStyleC
|
theButton=self.btnSingleStyleC
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -967,7 +979,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Double quote open style",
|
"Double quote open style",
|
||||||
self.quoteSym["DO"],
|
self.quoteSym["DO"],
|
||||||
"Auto-replaces straight quotes before words.",
|
"The symbol to use for a leading double quote.",
|
||||||
theButton=self.btnDoubleStyleO
|
theButton=self.btnDoubleStyleO
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -983,7 +995,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
"Double quote close style",
|
"Double quote close style",
|
||||||
self.quoteSym["DC"],
|
self.quoteSym["DC"],
|
||||||
"Auto-replaces straight quotes after words.",
|
"The symbol to use for a trailing double quote.",
|
||||||
theButton=self.btnDoubleStyleC
|
theButton=self.btnDoubleStyleC
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -286,36 +286,36 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.error("No project open")
|
logger.error("No project open")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
|
if qApp.focusWidget() != self and self.mainConf.showGUI:
|
||||||
if hasFocus and self.theParent.hasProject:
|
return False
|
||||||
|
|
||||||
tHandle = self.getSelectedHandle()
|
tHandle = self.getSelectedHandle()
|
||||||
tItem = self._getTreeItem(tHandle)
|
tItem = self._getTreeItem(tHandle)
|
||||||
pItem = tItem.parent()
|
if tItem is None:
|
||||||
if pItem is None:
|
return False
|
||||||
tIndex = self.indexOfTopLevelItem(tItem)
|
|
||||||
nChild = self.topLevelItemCount()
|
|
||||||
nIndex = tIndex + nStep
|
|
||||||
if nIndex < 0 or nIndex >= nChild:
|
|
||||||
return False
|
|
||||||
cItem = self.takeTopLevelItem(tIndex)
|
|
||||||
self.insertTopLevelItem(nIndex, cItem)
|
|
||||||
|
|
||||||
else:
|
pItem = tItem.parent()
|
||||||
tIndex = pItem.indexOfChild(tItem)
|
if pItem is None:
|
||||||
nChild = pItem.childCount()
|
tIndex = self.indexOfTopLevelItem(tItem)
|
||||||
nIndex = tIndex + nStep
|
nChild = self.topLevelItemCount()
|
||||||
if nIndex < 0 or nIndex >= nChild:
|
nIndex = tIndex + nStep
|
||||||
return False
|
if nIndex < 0 or nIndex >= nChild:
|
||||||
cItem = pItem.takeChild(tIndex)
|
return False
|
||||||
pItem.insertChild(nIndex, cItem)
|
cItem = self.takeTopLevelItem(tIndex)
|
||||||
|
self.insertTopLevelItem(nIndex, cItem)
|
||||||
self.clearSelection()
|
|
||||||
cItem.setSelected(True)
|
|
||||||
self._setTreeChanged(True)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return False
|
tIndex = pItem.indexOfChild(tItem)
|
||||||
|
nChild = pItem.childCount()
|
||||||
|
nIndex = tIndex + nStep
|
||||||
|
if nIndex < 0 or nIndex >= nChild:
|
||||||
|
return False
|
||||||
|
cItem = pItem.takeChild(tIndex)
|
||||||
|
pItem.insertChild(nIndex, cItem)
|
||||||
|
|
||||||
|
self.clearSelection()
|
||||||
|
cItem.setSelected(True)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
trItemS = self._getTreeItem(tHandle)
|
trItemS = self._getTreeItem(tHandle)
|
||||||
nwItemS = self.theProject.projTree[tHandle]
|
nwItemS = self.theProject.projTree[tHandle]
|
||||||
|
|
||||||
if nwItemS is None:
|
if trItemS is None or nwItemS is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
|
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
|
||||||
@@ -582,18 +582,23 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
properly reported to the function.
|
properly reported to the function.
|
||||||
"""
|
"""
|
||||||
tItem = self._getTreeItem(tHandle)
|
tItem = self._getTreeItem(tHandle)
|
||||||
if tItem is not None:
|
if tItem is None:
|
||||||
tItem.setText(self.C_COUNT, f"{theCount:n}")
|
return
|
||||||
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
|
|
||||||
pItem = tItem.parent()
|
|
||||||
if pItem is not None:
|
|
||||||
pCount = 0
|
|
||||||
for i in range(pItem.childCount()):
|
|
||||||
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
|
|
||||||
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
|
|
||||||
|
|
||||||
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
|
tItem.setText(self.C_COUNT, f"{theCount:n}")
|
||||||
self.propagateCount(pHandle, pCount, nDepth+1)
|
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
|
||||||
|
|
||||||
|
pItem = tItem.parent()
|
||||||
|
if pItem is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
pCount = 0
|
||||||
|
for i in range(pItem.childCount()):
|
||||||
|
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
|
||||||
|
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
|
||||||
|
|
||||||
|
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
|
||||||
|
self.propagateCount(pHandle, pCount, nDepth+1)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1024,12 +1029,12 @@ class GuiProjectTreeMenu(QMenu):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def filterActions(self, theItem):
|
def filterActions(self, theItem):
|
||||||
"""Update item settings from the nwItem.
|
"""Filter the menu entries available based on the properties of
|
||||||
|
the item the menu was activated on.
|
||||||
"""
|
"""
|
||||||
self.theItem = theItem
|
self.theItem = theItem
|
||||||
theRoot = self.theTree.theProject.projTree.getRootItem(theItem.itemHandle)
|
|
||||||
|
|
||||||
if theItem is None or theRoot is None:
|
if theItem is None:
|
||||||
logger.error("Failed to extract information to build tree context menu")
|
logger.error("Failed to extract information to build tree context menu")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1038,14 +1043,13 @@ class GuiProjectTreeMenu(QMenu):
|
|||||||
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
|
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
|
||||||
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
|
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
|
||||||
isFile = theItem.itemType == nwItemType.FILE
|
isFile = theItem.itemType == nwItemType.FILE
|
||||||
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
|
|
||||||
isOrph = isFile and theItem.itemParent is None
|
isOrph = isFile and theItem.itemParent is None
|
||||||
|
|
||||||
showOpen = isFile
|
showOpen = isFile
|
||||||
showView = isFile
|
showView = isFile
|
||||||
showEdit = not isTrash and not isOrph
|
showEdit = not isTrash and not isOrph
|
||||||
showExport = isFile
|
showExport = isFile
|
||||||
showNewFile = not (isTrash or inTrash or isOrph or isArch)
|
showNewFile = not (isTrash or inTrash or isOrph)
|
||||||
showNewFolder = not (isTrash or inTrash or isOrph)
|
showNewFolder = not (isTrash or inTrash or isOrph)
|
||||||
showDelete = not isTrash
|
showDelete = not isTrash
|
||||||
showEmpty = isTrash
|
showEmpty = isTrash
|
||||||
|
|||||||
@@ -281,11 +281,11 @@ class GuiTheme:
|
|||||||
cnfSec = "Main"
|
cnfSec = "Main"
|
||||||
if confParser.has_section(cnfSec):
|
if confParser.has_section(cnfSec):
|
||||||
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
||||||
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
|
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "N/A")
|
||||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
|
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
|
||||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
|
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
|
||||||
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
||||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
|
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
|
||||||
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
||||||
|
|
||||||
## Palette
|
## Palette
|
||||||
@@ -638,10 +638,10 @@ class GuiIcons:
|
|||||||
if confParser.has_section(cnfSec):
|
if confParser.has_section(cnfSec):
|
||||||
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
||||||
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
|
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
|
||||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
|
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
|
||||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
|
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
|
||||||
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
||||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
|
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
|
||||||
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
||||||
|
|
||||||
## Palette
|
## Palette
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ class GuiMain(QMainWindow):
|
|||||||
# Core Classes
|
# Core Classes
|
||||||
# ============
|
# ============
|
||||||
|
|
||||||
# Core Classes and settings
|
# Core Classes and Settings
|
||||||
self.theTheme = GuiTheme(self)
|
self.theTheme = GuiTheme(self)
|
||||||
self.theProject = NWProject(self)
|
self.theProject = NWProject(self)
|
||||||
self.theIndex = NWIndex(self.theProject, self)
|
self.theIndex = NWIndex(self.theProject, self)
|
||||||
self.hasProject = False
|
self.hasProject = False
|
||||||
self.isFocusMode = False
|
self.isFocusMode = False
|
||||||
|
|
||||||
# Prepare main window
|
# Prepare Main Window
|
||||||
self.resize(*self.mainConf.getWinSize())
|
self.resize(*self.mainConf.getWinSize())
|
||||||
self._setWindowTitle()
|
self._setWindowTitle()
|
||||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||||
@@ -238,6 +238,12 @@ class GuiMain(QMainWindow):
|
|||||||
if self.mainConf.showGUI:
|
if self.mainConf.showGUI:
|
||||||
self.showProjectLoadDialog()
|
self.showProjectLoadDialog()
|
||||||
|
|
||||||
|
# Show the latest release notes, if they haven't been shown before
|
||||||
|
if self.mainConf.lastNotes != nw.__version__:
|
||||||
|
if self.mainConf.showGUI:
|
||||||
|
self.showAboutNWDialog(showNotes=True)
|
||||||
|
self.mainConf.lastNotes = nw.__version__
|
||||||
|
|
||||||
logger.debug("novelWriter is ready ...")
|
logger.debug("novelWriter is ready ...")
|
||||||
self.setStatus("novelWriter is ready ...")
|
self.setStatus("novelWriter is ready ...")
|
||||||
|
|
||||||
@@ -915,11 +921,18 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def showAboutNWDialog(self):
|
def showAboutNWDialog(self, showNotes=False):
|
||||||
"""Show the about dialog for novelWriter.
|
"""Show the about dialog for novelWriter.
|
||||||
"""
|
"""
|
||||||
dlgAbout = GuiAbout(self)
|
dlgAbout = GuiAbout(self)
|
||||||
dlgAbout.exec_()
|
dlgAbout.setModal(True)
|
||||||
|
dlgAbout.show()
|
||||||
|
qApp.processEvents()
|
||||||
|
dlgAbout.populateGUI()
|
||||||
|
|
||||||
|
if showNotes:
|
||||||
|
dlgAbout.showReleaseNotes()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def showAboutQtDialog(self):
|
def showAboutQtDialog(self):
|
||||||
@@ -969,6 +982,13 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def askQuestion(self, theTitle, theQuestion):
|
||||||
|
"""Ask the user a Yes/No question.
|
||||||
|
"""
|
||||||
|
msgBox = QMessageBox()
|
||||||
|
msgRes = msgBox.question(self, theTitle, theQuestion)
|
||||||
|
return msgRes == QMessageBox.Yes
|
||||||
|
|
||||||
def reportConfErr(self):
|
def reportConfErr(self):
|
||||||
"""Checks if the Config module has any errors to report, and let
|
"""Checks if the Config module has any errors to report, and let
|
||||||
the user know if this is the case. The Config module caches
|
the user know if this is the case. The Config module caches
|
||||||
@@ -1060,10 +1080,10 @@ class GuiMain(QMainWindow):
|
|||||||
|
|
||||||
self.isFocusMode = not self.isFocusMode
|
self.isFocusMode = not self.isFocusMode
|
||||||
if self.isFocusMode:
|
if self.isFocusMode:
|
||||||
logger.debug("Activating Focus mode")
|
logger.debug("Activating Focus Mode")
|
||||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||||
else:
|
else:
|
||||||
logger.debug("Deactivating Focus mode")
|
logger.debug("Deactivating Focus Mode")
|
||||||
|
|
||||||
isVisible = not self.isFocusMode
|
isVisible = not self.isFocusMode
|
||||||
self.treePane.setVisible(isVisible)
|
self.treePane.setVisible(isVisible)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
markers =
|
markers =
|
||||||
project: Project classes tests
|
base: Base functionality tests
|
||||||
error: Test various error handling scenarios
|
|
||||||
core: Core functionality tests
|
core: Core functionality tests
|
||||||
gui: Qt5 GUI tests
|
gui: Qt5 GUI tests
|
||||||
serial
|
serial
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -27,11 +27,12 @@ UsedUserAreasWarning=no
|
|||||||
;PrivilegesRequired=lowest
|
;PrivilegesRequired=lowest
|
||||||
PrivilegesRequiredOverridesAllowed=dialog
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
OutputDir={#nwAppDir}
|
OutputDir={#nwAppDir}
|
||||||
OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup
|
OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
ChangesAssociations=yes
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
@@ -50,3 +51,10 @@ Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Fil
|
|||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue
|
||||||
|
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey
|
||||||
|
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico"
|
||||||
|
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\novelWriter.exe"" ""%1"""
|
||||||
|
Root: HKA; Subkey: "Software\Classes\Applications\novelWriter.exe\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# novelWriter Tests
|
||||||
|
|
||||||
|
The test suite uses PyTest for testing.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* `python3-pytest` for the basic framework (required)
|
||||||
|
* `python3-pytestqt` for Qt support (required)
|
||||||
|
* `python3-pytest-cov` for code coverage reports (optional)
|
||||||
|
* `python3-pytest-xvfb` for headless tests (optional)
|
||||||
|
|
||||||
|
## HowTo
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
To run all tests, type:
|
||||||
|
```bash
|
||||||
|
pytest-3 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-v` switch enables verbose mode, with one test per line.
|
||||||
|
For a more compact view, omit this switch.
|
||||||
|
|
||||||
|
### Headless
|
||||||
|
|
||||||
|
To run tests in headless mode, either use the Qt `offscreen` mode:
|
||||||
|
```bash
|
||||||
|
export QT_QPA_PLATFORM=offscreen
|
||||||
|
```
|
||||||
|
|
||||||
|
or run with `xvfb`:
|
||||||
|
```bash
|
||||||
|
xvfb-run pytest-3 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
To add test coverage, run the following:
|
||||||
|
```bash
|
||||||
|
pytest-3 -v --cov=nw --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--cov-report` switch generates an html report, omit it to print a coverage summary to the terminal.
|
||||||
|
The html coverage report will be available in the `htmlcov` folder.
|
||||||
|
|
||||||
|
### Test Markers (Categories)
|
||||||
|
|
||||||
|
To run with specific test markers, add the `-m` switch:
|
||||||
|
```bash
|
||||||
|
pytest-3 -v -m core
|
||||||
|
```
|
||||||
|
|
||||||
|
Available markers are:
|
||||||
|
|
||||||
|
* '`core`' for unit tests covering the classes in the `nw/core` folder
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
To filter specific groups of tests, use the `-k` switch.
|
||||||
|
The commands for the respective test categories are listed below.
|
||||||
|
|
||||||
|
| Type | Test Target | Source File(s) | Marker | Filter |
|
||||||
|
| :--- | :----------------- | :-------------------- | :-------- | :-------------------- |
|
||||||
|
| Unit | Main function | nw/\_\_init\_\_.py | `-m base` | `-k testBaseInit` |
|
||||||
|
| Unit | Common functions | nw/common.py | `-m base` | `-k testBaseCommon` |
|
||||||
|
| Unit | Config class | nw/config.py | `-m base` | `-k testBaseConfig` |
|
||||||
|
| Unit | Error handlers | nw/error.py | `-m base` | `-k testBaseError` |
|
||||||
|
| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` |
|
||||||
|
| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` |
|
||||||
|
| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` |
|
||||||
|
| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` |
|
||||||
|
| Unit | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` |
|
||||||
|
| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` |
|
||||||
|
| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` |
|
||||||
|
| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` |
|
||||||
|
| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` |
|
||||||
|
| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` |
|
||||||
|
| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` |
|
||||||
@@ -7,7 +7,7 @@ import pytest
|
|||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from nwdummy import DummyMain
|
from dummy import DummyMain
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
@@ -20,94 +20,42 @@ from nw.config import Config # noqa: E402
|
|||||||
##
|
##
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def nwTemp():
|
def tmpDir():
|
||||||
"""A temporary folder for the test session. This folder is
|
"""A temporary folder for the test session. This folder is
|
||||||
presistent after the test so that the status of generated files can
|
presistent after the test so that the status of generated files can
|
||||||
be checked. The folder is instead cleared before a new test session.
|
be checked. The folder is instead cleared before a new test session.
|
||||||
"""
|
"""
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
tempDir = os.path.join(testDir, "temp")
|
theDir = os.path.join(testDir, "temp")
|
||||||
if os.path.isdir(tempDir):
|
if os.path.isdir(theDir):
|
||||||
shutil.rmtree(tempDir)
|
shutil.rmtree(theDir)
|
||||||
if not os.path.isdir(tempDir):
|
if not os.path.isdir(theDir):
|
||||||
os.mkdir(tempDir)
|
os.mkdir(theDir)
|
||||||
return tempDir
|
return theDir
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def nwRef():
|
def refDir():
|
||||||
"""The folder where all the reference files are stored for verifying
|
"""The folder where all the reference files are stored for verifying
|
||||||
the results of tests.
|
the results of tests.
|
||||||
"""
|
"""
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
refDir = os.path.join(testDir, "reference")
|
theDir = os.path.join(testDir, "reference")
|
||||||
return refDir
|
return theDir
|
||||||
|
|
||||||
##
|
|
||||||
# novelWriter Objects
|
|
||||||
##
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmpConf(nwTemp):
|
def outDir(tmpDir):
|
||||||
"""Create a temporary novelWriter configuration object.
|
"""An output folder for test results
|
||||||
"""
|
"""
|
||||||
theConf = Config()
|
theDir = os.path.join(tmpDir, "results")
|
||||||
theConf.initConfig(nwTemp, nwTemp)
|
if not os.path.isdir(theDir):
|
||||||
theConf.setLastPath("")
|
os.mkdir(theDir)
|
||||||
return theConf
|
return theDir
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def nwConf(nwRef, nwTemp):
|
|
||||||
"""Temporary novelWriter configuration used for the dummy instance
|
|
||||||
of novelWriter's main GUI.
|
|
||||||
"""
|
|
||||||
theConf = Config()
|
|
||||||
theConf.initConfig(nwRef, nwTemp)
|
|
||||||
return theConf
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def nwDummy(nwRef, nwTemp, nwConf):
|
|
||||||
"""Create a dummy instance of novelWriter's main GUI class.
|
|
||||||
"""
|
|
||||||
theDummy = DummyMain()
|
|
||||||
theDummy.mainConf = nwConf
|
|
||||||
return theDummy
|
|
||||||
|
|
||||||
##
|
|
||||||
# Temporary Test Folders
|
|
||||||
##
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def nwTempProj(nwTemp):
|
|
||||||
"""A temporary folder for project tests.
|
|
||||||
"""
|
|
||||||
projDir = os.path.join(nwTemp, "proj")
|
|
||||||
if not os.path.isdir(projDir):
|
|
||||||
os.mkdir(projDir)
|
|
||||||
return projDir
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def nwTempGUI(nwTemp):
|
|
||||||
"""A temporary folder for GUI tests.
|
|
||||||
"""
|
|
||||||
guiDir = os.path.join(nwTemp, "gui")
|
|
||||||
if not os.path.isdir(guiDir):
|
|
||||||
os.mkdir(guiDir)
|
|
||||||
return guiDir
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def nwTempBuild(nwTemp):
|
|
||||||
"""A temporary folder for build tests.
|
|
||||||
"""
|
|
||||||
buildDir = os.path.join(nwTemp, "build")
|
|
||||||
if not os.path.isdir(buildDir):
|
|
||||||
os.mkdir(buildDir)
|
|
||||||
return buildDir
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def nwFuncTemp(nwTemp):
|
def fncDir(tmpDir):
|
||||||
"""A temporary folder for a single test function.
|
"""A temporary folder for a single test function.
|
||||||
"""
|
"""
|
||||||
funcDir = os.path.join(nwTemp, "ftemp")
|
funcDir = os.path.join(tmpDir, "ftemp")
|
||||||
if os.path.isdir(funcDir):
|
if os.path.isdir(funcDir):
|
||||||
shutil.rmtree(funcDir)
|
shutil.rmtree(funcDir)
|
||||||
if not os.path.isdir(funcDir):
|
if not os.path.isdir(funcDir):
|
||||||
@@ -118,16 +66,40 @@ def nwFuncTemp(nwTemp):
|
|||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Temp Folders for Projects
|
# novelWriter Objects
|
||||||
##
|
##
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def nwMinimal(nwTemp):
|
def tmpConf(tmpDir):
|
||||||
|
"""Create a temporary novelWriter configuration object.
|
||||||
|
"""
|
||||||
|
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||||
|
if os.path.isfile(confFile):
|
||||||
|
os.unlink(confFile)
|
||||||
|
theConf = Config()
|
||||||
|
theConf.initConfig(tmpDir, tmpDir)
|
||||||
|
theConf.setLastPath("")
|
||||||
|
return theConf
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def dummyGUI(tmpConf):
|
||||||
|
"""Create a dummy instance of novelWriter's main GUI class.
|
||||||
|
"""
|
||||||
|
theDummy = DummyMain()
|
||||||
|
theDummy.mainConf = tmpConf
|
||||||
|
return theDummy
|
||||||
|
|
||||||
|
##
|
||||||
|
# Temp Project Folders
|
||||||
|
##
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def nwMinimal(tmpDir):
|
||||||
"""A minimal novelWriter example project.
|
"""A minimal novelWriter example project.
|
||||||
"""
|
"""
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
minimalStore = os.path.join(testDir, "minimal")
|
minimalStore = os.path.join(testDir, "minimal")
|
||||||
minimalDir = os.path.join(nwTemp, "minimal")
|
minimalDir = os.path.join(tmpDir, "minimal")
|
||||||
if os.path.isdir(minimalDir):
|
if os.path.isdir(minimalDir):
|
||||||
shutil.rmtree(minimalDir)
|
shutil.rmtree(minimalDir)
|
||||||
shutil.copytree(minimalStore, minimalDir)
|
shutil.copytree(minimalStore, minimalDir)
|
||||||
@@ -143,13 +115,13 @@ def nwMinimal(nwTemp):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def nwLipsum(nwTemp):
|
def nwLipsum(tmpDir):
|
||||||
"""A medium sized novelWriter example project with a lot of Lorem
|
"""A medium sized novelWriter example project with a lot of Lorem
|
||||||
Ipsum dummy text.
|
Ipsum dummy text.
|
||||||
"""
|
"""
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
lipsumStore = os.path.join(testDir, "lipsum")
|
lipsumStore = os.path.join(testDir, "lipsum")
|
||||||
lipsumDir = os.path.join(nwTemp, "lipsum")
|
lipsumDir = os.path.join(tmpDir, "lipsum")
|
||||||
if os.path.isdir(lipsumDir):
|
if os.path.isdir(lipsumDir):
|
||||||
shutil.rmtree(lipsumDir)
|
shutil.rmtree(lipsumDir)
|
||||||
shutil.copytree(lipsumStore, lipsumDir)
|
shutil.copytree(lipsumStore, lipsumDir)
|
||||||
@@ -165,12 +137,12 @@ def nwLipsum(nwTemp):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def nwOldProj(nwTemp):
|
def nwOldProj(tmpDir):
|
||||||
"""A minimal movelWriter project using the old folder structure.
|
"""A minimal movelWriter project using the old folder structure.
|
||||||
"""
|
"""
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
oldProjStore = os.path.join(testDir, "oldproj")
|
oldProjStore = os.path.join(testDir, "oldproj")
|
||||||
oldProjDir = os.path.join(nwTemp, "oldproj")
|
oldProjDir = os.path.join(tmpDir, "oldproj")
|
||||||
if os.path.isdir(oldProjDir):
|
if os.path.isdir(oldProjDir):
|
||||||
shutil.rmtree(oldProjDir)
|
shutil.rmtree(oldProjDir)
|
||||||
shutil.copytree(oldProjStore, oldProjDir)
|
shutil.copytree(oldProjStore, oldProjDir)
|
||||||
@@ -199,4 +171,30 @@ def yesToAll(monkeypatch):
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes
|
QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes
|
||||||
)
|
)
|
||||||
|
yield
|
||||||
|
monkeypatch.undo()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# =============================================================================================== #
|
||||||
|
|
||||||
|
##
|
||||||
|
# Temporary Test Folders
|
||||||
|
##
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def nwTempGUI(tmpDir):
|
||||||
|
"""A temporary folder for GUI tests.
|
||||||
|
"""
|
||||||
|
guiDir = os.path.join(tmpDir, "gui")
|
||||||
|
if not os.path.isdir(guiDir):
|
||||||
|
os.mkdir(guiDir)
|
||||||
|
return guiDir
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def nwTempBuild(tmpDir):
|
||||||
|
"""A temporary folder for build tests.
|
||||||
|
"""
|
||||||
|
buildDir = os.path.join(tmpDir, "build")
|
||||||
|
if not os.path.isdir(buildDir):
|
||||||
|
os.mkdir(buildDir)
|
||||||
|
return buildDir
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Test Dummy GUI Classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
# Mock GUI
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
|
class DummyMain():
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.mainConf = None
|
||||||
|
self.hasProject = True
|
||||||
|
self.theIndex = None
|
||||||
|
self.theProject = None
|
||||||
|
self.statusBar = DummyStatusBar()
|
||||||
|
|
||||||
|
# Test Variables
|
||||||
|
self.askResponse = True
|
||||||
|
self.lastAlert = ""
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def makeAlert(self, theMessage, theLevel):
|
||||||
|
print("%s: %s" % (str(theLevel), theMessage))
|
||||||
|
self.lastAlert = str(theMessage)
|
||||||
|
return
|
||||||
|
|
||||||
|
def askQuestion(self, theTitle, theQustion):
|
||||||
|
print("Question: %s" % theQustion)
|
||||||
|
return self.askResponse
|
||||||
|
|
||||||
|
def setStatus(self, theMessage):
|
||||||
|
return
|
||||||
|
|
||||||
|
def setProjectStatus(self, isChanged):
|
||||||
|
return
|
||||||
|
|
||||||
|
def openProject(self, projPath):
|
||||||
|
return
|
||||||
|
|
||||||
|
def rebuildIndex(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def closeMain(self):
|
||||||
|
return "closeMain"
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return "close"
|
||||||
|
|
||||||
|
# Test Functions
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.askResponse = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.lastAlert = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class DummyMain
|
||||||
|
|
||||||
|
class DummyStatusBar():
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def setStatus(self, theText):
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class DummyStatusBar
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
# Error Functions
|
||||||
|
# Dummy functions that will raise errors instead.
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
|
def causeOSError(*args, **kwargs):
|
||||||
|
raise OSError
|
||||||
|
|
||||||
|
def causeException(*args, **kwargs):
|
||||||
|
raise Exception
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
[
|
|
||||||
[
|
|
||||||
"content/04468803b92e1.nwd",
|
|
||||||
"WORLD",
|
|
||||||
"Ancient Europe"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/2426c6f0ca922.nwd",
|
|
||||||
"PLOT",
|
|
||||||
"Main"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/441420a886d82.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Chapter Two"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/47666c91c7ccf.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Scene Five"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/4c4f28287af27.nwd",
|
|
||||||
"CHARACTER",
|
|
||||||
"Mr. Nobody"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/7a992350f3eb6.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Lorem Ipsum"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/846352075de7d.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Interlude"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/88243afbe5ed8.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Scene One"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/88d59a277361b.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Prologue"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/8c58a65414c23.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Front Matter"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/db7e733775d4d.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Act One"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/eb103bc70c90c.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Scene Three"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/f8c0562e50f1b.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Scene Four"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/f96ec11c6a3da.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Scene Two"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/fb609cd8319dc.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Chapter One"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[
|
|
||||||
[
|
|
||||||
"content/8c659a11cd429.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"New Scene"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/a35baf2e93843.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"Title Page"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"content/f5ab3e30151e1.nwd",
|
|
||||||
"NOVEL",
|
|
||||||
"New Chapter"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""novelWriter Test Dummy GUI Classes
|
|
||||||
"""
|
|
||||||
|
|
||||||
class DummyMain():
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.mainConf = None
|
|
||||||
self.hasProject = True
|
|
||||||
self.statusBar = StatusBar()
|
|
||||||
return
|
|
||||||
|
|
||||||
def makeAlert(self, theMessage, theLevel):
|
|
||||||
print("%s: %s" % (str(theLevel), theMessage))
|
|
||||||
return
|
|
||||||
|
|
||||||
def setStatus(self, theMessage):
|
|
||||||
return
|
|
||||||
|
|
||||||
def setProjectStatus(self, isChanged):
|
|
||||||
return
|
|
||||||
|
|
||||||
def openProject(self, projPath):
|
|
||||||
return
|
|
||||||
|
|
||||||
def rebuildIndex(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
# END Class GuiMain
|
|
||||||
|
|
||||||
class StatusBar():
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def setStatus(self, theText):
|
|
||||||
return
|
|
||||||
|
|
||||||
# END Class StatusBar
|
|
||||||
@@ -6,6 +6,7 @@ icons = typicons_colour_light
|
|||||||
guidark = False
|
guidark = False
|
||||||
guifont =
|
guifont =
|
||||||
guifontsize = 11
|
guifontsize = 11
|
||||||
|
lastnotes = 1.0
|
||||||
|
|
||||||
[Sizes]
|
[Sizes]
|
||||||
geometry = 1200, 650
|
geometry = 1200, 650
|
||||||
@@ -41,56 +41,56 @@
|
|||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="23">
|
<content count="23">
|
||||||
<item handle="73475cb40a568" order="None" parent="None">
|
<item handle="73475cb40a568" order="0" parent="None">
|
||||||
<name>Novel</name>
|
<name>Novel</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="44cb730c42048" order="None" parent="None">
|
<item handle="44cb730c42048" order="0" parent="None">
|
||||||
<name>Plot</name>
|
<name>Plot</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>PLOT</class>
|
<class>PLOT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||||
<name>Characters</name>
|
<name>Characters</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="811786ad1ae74" order="None" parent="None">
|
<item handle="811786ad1ae74" order="0" parent="None">
|
||||||
<name>Locations</name>
|
<name>Locations</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>WORLD</class>
|
<class>WORLD</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="25fc0e7096fc6" order="None" parent="None">
|
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||||
<name>Timeline</name>
|
<name>Timeline</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>TIMELINE</class>
|
<class>TIMELINE</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="31489056e0916" order="None" parent="None">
|
<item handle="31489056e0916" order="0" parent="None">
|
||||||
<name>Objects</name>
|
<name>Objects</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>OBJECT</class>
|
<class>OBJECT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="98010bd9270f9" order="None" parent="None">
|
<item handle="98010bd9270f9" order="0" parent="None">
|
||||||
<name>Entity</name>
|
<name>Entity</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>ENTITY</class>
|
<class>ENTITY</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
|
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||||
<name>Title Page</name>
|
<name>Title Page</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -102,14 +102,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
|
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||||
<name>Chapter 1</name>
|
<name>Chapter 1</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="031b4af5197ec" order="None" parent="1a6562590ef19">
|
<item handle="031b4af5197ec" order="0" parent="1a6562590ef19">
|
||||||
<name>Chapter 1</name>
|
<name>Chapter 1</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="41cfc0d1f2d12" order="None" parent="1a6562590ef19">
|
<item handle="41cfc0d1f2d12" order="0" parent="1a6562590ef19">
|
||||||
<name>Scene 1.1</name>
|
<name>Scene 1.1</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="2858dcd1057d3" order="None" parent="1a6562590ef19">
|
<item handle="2858dcd1057d3" order="0" parent="1a6562590ef19">
|
||||||
<name>Scene 1.2</name>
|
<name>Scene 1.2</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="2fca346db6561" order="None" parent="1a6562590ef19">
|
<item handle="2fca346db6561" order="0" parent="1a6562590ef19">
|
||||||
<name>Scene 1.3</name>
|
<name>Scene 1.3</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -157,14 +157,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
|
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||||
<name>Chapter 2</name>
|
<name>Chapter 2</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="7688b6ef52555" order="None" parent="02d20bbd7e394">
|
<item handle="7688b6ef52555" order="0" parent="02d20bbd7e394">
|
||||||
<name>Chapter 2</name>
|
<name>Chapter 2</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="c837649cce43f" order="None" parent="02d20bbd7e394">
|
<item handle="c837649cce43f" order="0" parent="02d20bbd7e394">
|
||||||
<name>Scene 2.1</name>
|
<name>Scene 2.1</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="6208ef0f7750c" order="None" parent="02d20bbd7e394">
|
<item handle="6208ef0f7750c" order="0" parent="02d20bbd7e394">
|
||||||
<name>Scene 2.2</name>
|
<name>Scene 2.2</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="3e1e967e9b793" order="None" parent="02d20bbd7e394">
|
<item handle="3e1e967e9b793" order="0" parent="02d20bbd7e394">
|
||||||
<name>Scene 2.3</name>
|
<name>Scene 2.3</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -212,14 +212,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="39fa9ec190eee" order="None" parent="73475cb40a568">
|
<item handle="39fa9ec190eee" order="0" parent="73475cb40a568">
|
||||||
<name>Chapter 3</name>
|
<name>Chapter 3</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="d029fa3a95e17" order="None" parent="39fa9ec190eee">
|
<item handle="d029fa3a95e17" order="0" parent="39fa9ec190eee">
|
||||||
<name>Chapter 3</name>
|
<name>Chapter 3</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="81b8a03f97e87" order="None" parent="39fa9ec190eee">
|
<item handle="81b8a03f97e87" order="0" parent="39fa9ec190eee">
|
||||||
<name>Scene 3.1</name>
|
<name>Scene 3.1</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="da4ea2a5506f2" order="None" parent="39fa9ec190eee">
|
<item handle="da4ea2a5506f2" order="0" parent="39fa9ec190eee">
|
||||||
<name>Scene 3.2</name>
|
<name>Scene 3.2</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="a68b412c42825" order="None" parent="39fa9ec190eee">
|
<item handle="a68b412c42825" order="0" parent="39fa9ec190eee">
|
||||||
<name>Scene 3.3</name>
|
<name>Scene 3.3</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -41,56 +41,56 @@
|
|||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="14">
|
<content count="14">
|
||||||
<item handle="73475cb40a568" order="None" parent="None">
|
<item handle="73475cb40a568" order="0" parent="None">
|
||||||
<name>Novel</name>
|
<name>Novel</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="44cb730c42048" order="None" parent="None">
|
<item handle="44cb730c42048" order="0" parent="None">
|
||||||
<name>Plot</name>
|
<name>Plot</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>PLOT</class>
|
<class>PLOT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||||
<name>Characters</name>
|
<name>Characters</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="811786ad1ae74" order="None" parent="None">
|
<item handle="811786ad1ae74" order="0" parent="None">
|
||||||
<name>Locations</name>
|
<name>Locations</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>WORLD</class>
|
<class>WORLD</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="25fc0e7096fc6" order="None" parent="None">
|
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||||
<name>Timeline</name>
|
<name>Timeline</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>TIMELINE</class>
|
<class>TIMELINE</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="31489056e0916" order="None" parent="None">
|
<item handle="31489056e0916" order="0" parent="None">
|
||||||
<name>Objects</name>
|
<name>Objects</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>OBJECT</class>
|
<class>OBJECT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="98010bd9270f9" order="None" parent="None">
|
<item handle="98010bd9270f9" order="0" parent="None">
|
||||||
<name>Entity</name>
|
<name>Entity</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>ENTITY</class>
|
<class>ENTITY</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
|
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||||
<name>Title Page</name>
|
<name>Title Page</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
|
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 1</name>
|
<name>Scene 1</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="031b4af5197ec" order="None" parent="73475cb40a568">
|
<item handle="031b4af5197ec" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 2</name>
|
<name>Scene 2</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="41cfc0d1f2d12" order="None" parent="73475cb40a568">
|
<item handle="41cfc0d1f2d12" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 3</name>
|
<name>Scene 3</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="2858dcd1057d3" order="None" parent="73475cb40a568">
|
<item handle="2858dcd1057d3" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 4</name>
|
<name>Scene 4</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="2fca346db6561" order="None" parent="73475cb40a568">
|
<item handle="2fca346db6561" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 5</name>
|
<name>Scene 5</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
|
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||||
<name>Scene 6</name>
|
<name>Scene 6</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -39,35 +39,35 @@
|
|||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="10">
|
<content count="10">
|
||||||
<item handle="73475cb40a568" order="None" parent="None">
|
<item handle="73475cb40a568" order="0" parent="None">
|
||||||
<name>Novel</name>
|
<name>Novel</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="44cb730c42048" order="None" parent="None">
|
<item handle="44cb730c42048" order="0" parent="None">
|
||||||
<name>Plot</name>
|
<name>Plot</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>PLOT</class>
|
<class>PLOT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||||
<name>Characters</name>
|
<name>Characters</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="811786ad1ae74" order="None" parent="None">
|
<item handle="811786ad1ae74" order="0" parent="None">
|
||||||
<name>World</name>
|
<name>World</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>WORLD</class>
|
<class>WORLD</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||||
<name>Title Page</name>
|
<name>Title Page</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||||
<name>New Scene</name>
|
<name>New Scene</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="1a6562590ef19" order="None" parent="31489056e0916">
|
<item handle="1a6562590ef19" order="0" parent="31489056e0916">
|
||||||
<name>Hello</name>
|
<name>Hello</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="031b4af5197ec" order="None" parent="71ee45a3c0db9">
|
<item handle="031b4af5197ec" order="0" parent="71ee45a3c0db9">
|
||||||
<name>Jane</name>
|
<name>Jane</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
@@ -39,35 +39,35 @@
|
|||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="8">
|
<content count="8">
|
||||||
<item handle="73475cb40a568" order="None" parent="None">
|
<item handle="73475cb40a568" order="0" parent="None">
|
||||||
<name>Novel</name>
|
<name>Novel</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="44cb730c42048" order="None" parent="None">
|
<item handle="44cb730c42048" order="0" parent="None">
|
||||||
<name>Plot</name>
|
<name>Plot</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>PLOT</class>
|
<class>PLOT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||||
<name>Characters</name>
|
<name>Characters</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="811786ad1ae74" order="None" parent="None">
|
<item handle="811786ad1ae74" order="0" parent="None">
|
||||||
<name>World</name>
|
<name>World</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>WORLD</class>
|
<class>WORLD</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||||
<name>Title Page</name>
|
<name>Title Page</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||||
<name>New Scene</name>
|
<name>New Scene</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -39,35 +39,35 @@
|
|||||||
</importance>
|
</importance>
|
||||||
</settings>
|
</settings>
|
||||||
<content count="12">
|
<content count="12">
|
||||||
<item handle="73475cb40a568" order="None" parent="None">
|
<item handle="73475cb40a568" order="0" parent="None">
|
||||||
<name>Novel</name>
|
<name>Novel</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="44cb730c42048" order="None" parent="None">
|
<item handle="44cb730c42048" order="0" parent="None">
|
||||||
<name>Plot</name>
|
<name>Plot</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>PLOT</class>
|
<class>PLOT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||||
<name>Characters</name>
|
<name>Characters</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CHARACTER</class>
|
<class>CHARACTER</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="811786ad1ae74" order="None" parent="None">
|
<item handle="811786ad1ae74" order="0" parent="None">
|
||||||
<name>World</name>
|
<name>World</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>WORLD</class>
|
<class>WORLD</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||||
<name>Title Page</name>
|
<name>Title Page</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FOLDER</type>
|
<type>FOLDER</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||||
<name>New Chapter</name>
|
<name>New Chapter</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||||
<name>New Scene</name>
|
<name>New Scene</name>
|
||||||
<type>FILE</type>
|
<type>FILE</type>
|
||||||
<class>NOVEL</class>
|
<class>NOVEL</class>
|
||||||
@@ -110,28 +110,28 @@
|
|||||||
<paraCount>0</paraCount>
|
<paraCount>0</paraCount>
|
||||||
<cursorPos>0</cursorPos>
|
<cursorPos>0</cursorPos>
|
||||||
</item>
|
</item>
|
||||||
<item handle="1a6562590ef19" order="None" parent="None">
|
<item handle="1a6562590ef19" order="0" parent="None">
|
||||||
<name>Timeline</name>
|
<name>Timeline</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>TIMELINE</class>
|
<class>TIMELINE</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="031b4af5197ec" order="None" parent="None">
|
<item handle="031b4af5197ec" order="0" parent="None">
|
||||||
<name>Object</name>
|
<name>Object</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>OBJECT</class>
|
<class>OBJECT</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="41cfc0d1f2d12" order="None" parent="None">
|
<item handle="41cfc0d1f2d12" order="0" parent="None">
|
||||||
<name>Custom1</name>
|
<name>Custom1</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CUSTOM</class>
|
<class>CUSTOM</class>
|
||||||
<status>New</status>
|
<status>New</status>
|
||||||
<expanded>False</expanded>
|
<expanded>False</expanded>
|
||||||
</item>
|
</item>
|
||||||
<item handle="2858dcd1057d3" order="None" parent="None">
|
<item handle="2858dcd1057d3" order="0" parent="None">
|
||||||
<name>Custom2</name>
|
<name>Custom2</name>
|
||||||
<type>ROOT</type>
|
<type>ROOT</type>
|
||||||
<class>CUSTOM</class>
|
<class>CUSTOM</class>
|
||||||
@@ -6,6 +6,7 @@ icons = typicons_colour_light
|
|||||||
guidark = True
|
guidark = True
|
||||||
guifont = Cantarell
|
guifont = Cantarell
|
||||||
guifontsize = 12
|
guifontsize = 12
|
||||||
|
lastnotes = 1.0
|
||||||
|
|
||||||
[Sizes]
|
[Sizes]
|
||||||
geometry = 1100, 650
|
geometry = 1100, 650
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""novelWriter Common Class Tester
|
"""novelWriter Common Functions Tester
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@@ -9,10 +9,12 @@ from nw.common import (
|
|||||||
checkString, checkBool, checkInt, colRange, formatInt, transferCase,
|
checkString, checkBool, checkInt, colRange, formatInt, transferCase,
|
||||||
fuzzyTime, checkHandle, formatTimeStamp, formatTime
|
fuzzyTime, checkHandle, formatTimeStamp, formatTime
|
||||||
)
|
)
|
||||||
from nwtools import cmpList
|
from tools import cmpList
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.base
|
||||||
def testCheckString():
|
def testBaseCommon_CheckString():
|
||||||
|
"""Test the checkString function.
|
||||||
|
"""
|
||||||
assert checkString(None, "NotNone", True) is None
|
assert checkString(None, "NotNone", True) is None
|
||||||
assert checkString("None", "NotNone", True) is None
|
assert checkString("None", "NotNone", True) is None
|
||||||
assert checkString("None", "NotNone", False) == "None"
|
assert checkString("None", "NotNone", False) == "None"
|
||||||
@@ -21,8 +23,12 @@ def testCheckString():
|
|||||||
assert checkString(1.0, "NotNone", False) == "NotNone"
|
assert checkString(1.0, "NotNone", False) == "NotNone"
|
||||||
assert checkString(True, "NotNone", False) == "NotNone"
|
assert checkString(True, "NotNone", False) == "NotNone"
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_CheckString
|
||||||
def testCheckInt():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_CheckInt():
|
||||||
|
"""Test the checkInt function.
|
||||||
|
"""
|
||||||
assert checkInt(None, 3, True) is None
|
assert checkInt(None, 3, True) is None
|
||||||
assert checkInt("None", 3, True) is None
|
assert checkInt("None", 3, True) is None
|
||||||
assert checkInt(None, 3, False) == 3
|
assert checkInt(None, 3, False) == 3
|
||||||
@@ -30,8 +36,12 @@ def testCheckInt():
|
|||||||
assert checkInt(1.0, 3, False) == 1
|
assert checkInt(1.0, 3, False) == 1
|
||||||
assert checkInt(True, 3, False) == 1
|
assert checkInt(True, 3, False) == 1
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_CheckInt
|
||||||
def testCheckBool():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_CheckBool():
|
||||||
|
"""Test the checkBool function.
|
||||||
|
"""
|
||||||
assert checkBool(None, 3, True) is None
|
assert checkBool(None, 3, True) is None
|
||||||
assert checkBool("None", 3, True) is None
|
assert checkBool("None", 3, True) is None
|
||||||
assert checkBool("True", False, False)
|
assert checkBool("True", False, False)
|
||||||
@@ -44,8 +54,12 @@ def testCheckBool():
|
|||||||
assert checkBool(1.0, None, False) is None
|
assert checkBool(1.0, None, False) is None
|
||||||
assert checkBool(2.0, None, False) is None
|
assert checkBool(2.0, None, False) is None
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_CheckBool
|
||||||
def testCheckHandle():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_CheckHandle():
|
||||||
|
"""Test the checkHandle function.
|
||||||
|
"""
|
||||||
assert checkHandle("None", 1, True) is None
|
assert checkHandle("None", 1, True) is None
|
||||||
assert checkHandle("None", 1, False) == 1
|
assert checkHandle("None", 1, False) == 1
|
||||||
assert checkHandle(None, 1, True) is None
|
assert checkHandle(None, 1, True) is None
|
||||||
@@ -53,8 +67,12 @@ def testCheckHandle():
|
|||||||
assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf"
|
assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf"
|
||||||
assert checkHandle("h7666c91c7ccf", None, False) is None
|
assert checkHandle("h7666c91c7ccf", None, False) is None
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_CheckHandle
|
||||||
def testColRange():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_ColRange():
|
||||||
|
"""Test the colRange function.
|
||||||
|
"""
|
||||||
assert colRange([0, 0], [0, 0], 0) is None
|
assert colRange([0, 0], [0, 0], 0) is None
|
||||||
assert cmpList(
|
assert cmpList(
|
||||||
colRange([200, 50, 0], [50, 200, 0], 1),
|
colRange([200, 50, 0], [50, 200, 0], 1),
|
||||||
@@ -77,14 +95,22 @@ def testColRange():
|
|||||||
[[200, 50, 0], [162, 87, 0], [124, 124, 0], [86, 161, 0], [50, 200, 0]]
|
[[200, 50, 0], [162, 87, 0], [124, 124, 0], [86, 161, 0], [50, 200, 0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_ColRange
|
||||||
def testFormatTimeStamp():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_FormatTimeStamp():
|
||||||
|
"""Test the formatTimeStamp function.
|
||||||
|
"""
|
||||||
tTime = time.mktime(time.gmtime(0))
|
tTime = time.mktime(time.gmtime(0))
|
||||||
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
||||||
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_FormatTimeStamp
|
||||||
def testFormatTime():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_FormatTime():
|
||||||
|
"""Test the formatTime function.
|
||||||
|
"""
|
||||||
assert formatTime("1") == "ERROR"
|
assert formatTime("1") == "ERROR"
|
||||||
assert formatTime(1.0) == "ERROR"
|
assert formatTime(1.0) == "ERROR"
|
||||||
assert formatTime(1) == "00:00:01"
|
assert formatTime(1) == "00:00:01"
|
||||||
@@ -101,8 +127,12 @@ def testFormatTime():
|
|||||||
assert formatTime(86400) == "1-00:00:00"
|
assert formatTime(86400) == "1-00:00:00"
|
||||||
assert formatTime(360000) == "4-04:00:00"
|
assert formatTime(360000) == "4-04:00:00"
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_FormatTime
|
||||||
def testFormatInt():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_FormatInt():
|
||||||
|
"""Test the formatInt function.
|
||||||
|
"""
|
||||||
assert formatInt(1000) == "1000"
|
assert formatInt(1000) == "1000"
|
||||||
assert formatInt(1234) == "1.23\u2009k"
|
assert formatInt(1234) == "1.23\u2009k"
|
||||||
assert formatInt(12345) == "12.3\u2009k"
|
assert formatInt(12345) == "12.3\u2009k"
|
||||||
@@ -112,8 +142,12 @@ def testFormatInt():
|
|||||||
assert formatInt(123456789) == "123\u2009M"
|
assert formatInt(123456789) == "123\u2009M"
|
||||||
assert formatInt(1234567890) == "1.23\u2009G"
|
assert formatInt(1234567890) == "1.23\u2009G"
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_FormatInt
|
||||||
def testTransferCase():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_TransferCase():
|
||||||
|
"""Test the transferCase function.
|
||||||
|
"""
|
||||||
assert transferCase(1, "TaRgEt") == "TaRgEt"
|
assert transferCase(1, "TaRgEt") == "TaRgEt"
|
||||||
assert transferCase("source", 1) == 1
|
assert transferCase("source", 1) == 1
|
||||||
assert transferCase("", "TaRgEt") == "TaRgEt"
|
assert transferCase("", "TaRgEt") == "TaRgEt"
|
||||||
@@ -122,8 +156,12 @@ def testTransferCase():
|
|||||||
assert transferCase("SOURCE", "target") == "TARGET"
|
assert transferCase("SOURCE", "target") == "TARGET"
|
||||||
assert transferCase("source", "TARGET") == "target"
|
assert transferCase("source", "TARGET") == "target"
|
||||||
|
|
||||||
@pytest.mark.core
|
# END Test testBaseCommon_TransferCase
|
||||||
def testFuzzyTime():
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseCommon_FuzzyTime():
|
||||||
|
"""Test the fuzzyTime function.
|
||||||
|
"""
|
||||||
assert fuzzyTime(-1) == "in the future"
|
assert fuzzyTime(-1) == "in the future"
|
||||||
assert fuzzyTime(0) == "just now"
|
assert fuzzyTime(0) == "just now"
|
||||||
assert fuzzyTime(29) == "just now"
|
assert fuzzyTime(29) == "just now"
|
||||||
@@ -152,3 +190,5 @@ def testFuzzyTime():
|
|||||||
assert fuzzyTime(29808000) == "a year ago"
|
assert fuzzyTime(29808000) == "a year ago"
|
||||||
assert fuzzyTime(47336399) == "a year ago"
|
assert fuzzyTime(47336399) == "a year ago"
|
||||||
assert fuzzyTime(47336400) == "2 years ago"
|
assert fuzzyTime(47336400) == "2 years ago"
|
||||||
|
|
||||||
|
# END Test testBaseCommon_FuzzyTime
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Config Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
from dummy import causeOSError
|
||||||
|
from tools import cmpFiles
|
||||||
|
|
||||||
|
from nw.config import Config
|
||||||
|
from nw.constants import nwConst, nwFiles
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_Constructor(monkeypatch):
|
||||||
|
"""Test config contructor.
|
||||||
|
"""
|
||||||
|
# Linux
|
||||||
|
monkeypatch.setattr("sys.platform", "linux")
|
||||||
|
tstConf = Config()
|
||||||
|
assert tstConf.osLinux is True
|
||||||
|
assert tstConf.osDarwin is False
|
||||||
|
assert tstConf.osWindows is False
|
||||||
|
assert tstConf.osUnknown is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
monkeypatch.setattr("sys.platform", "darwin")
|
||||||
|
tstConf = Config()
|
||||||
|
assert tstConf.osLinux is False
|
||||||
|
assert tstConf.osDarwin is True
|
||||||
|
assert tstConf.osWindows is False
|
||||||
|
assert tstConf.osUnknown is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
monkeypatch.setattr("sys.platform", "win32")
|
||||||
|
tstConf = Config()
|
||||||
|
assert tstConf.osLinux is False
|
||||||
|
assert tstConf.osDarwin is False
|
||||||
|
assert tstConf.osWindows is True
|
||||||
|
assert tstConf.osUnknown is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Cygwin
|
||||||
|
monkeypatch.setattr("sys.platform", "cygwin")
|
||||||
|
tstConf = Config()
|
||||||
|
assert tstConf.osLinux is False
|
||||||
|
assert tstConf.osDarwin is False
|
||||||
|
assert tstConf.osWindows is True
|
||||||
|
assert tstConf.osUnknown is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Other
|
||||||
|
monkeypatch.setattr("sys.platform", "some_ther_os")
|
||||||
|
tstConf = Config()
|
||||||
|
assert tstConf.osLinux is False
|
||||||
|
assert tstConf.osDarwin is False
|
||||||
|
assert tstConf.osWindows is False
|
||||||
|
assert tstConf.osUnknown is True
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testBaseConfig_Constructor
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir):
|
||||||
|
"""Test config intialisation.
|
||||||
|
"""
|
||||||
|
tstConf = Config()
|
||||||
|
|
||||||
|
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||||
|
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
|
||||||
|
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
|
||||||
|
|
||||||
|
# Make sure we don't have any old conf file
|
||||||
|
if os.path.isfile(confFile):
|
||||||
|
os.unlink(confFile)
|
||||||
|
|
||||||
|
# Let the config class figure out the path
|
||||||
|
monkeypatch.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *args: fncDir)
|
||||||
|
tstConf.verQtValue = 50600
|
||||||
|
tstConf.initConfig()
|
||||||
|
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
|
||||||
|
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
|
||||||
|
assert not os.path.isfile(confFile)
|
||||||
|
tstConf.verQtValue = 50000
|
||||||
|
tstConf.initConfig()
|
||||||
|
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
|
||||||
|
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
|
||||||
|
assert not os.path.isfile(confFile)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Fail to make folders
|
||||||
|
monkeypatch.setattr("os.mkdir", causeOSError)
|
||||||
|
|
||||||
|
tstConfDir = os.path.join(fncDir, "test_conf")
|
||||||
|
tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir)
|
||||||
|
assert tstConf.confPath is None
|
||||||
|
assert tstConf.dataPath == tmpDir
|
||||||
|
assert not os.path.isfile(confFile)
|
||||||
|
|
||||||
|
tstDataDir = os.path.join(fncDir, "test_data")
|
||||||
|
tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir)
|
||||||
|
assert tstConf.confPath == tmpDir
|
||||||
|
assert tstConf.dataPath is None
|
||||||
|
assert os.path.isfile(confFile)
|
||||||
|
os.unlink(confFile)
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Test load/save with no path
|
||||||
|
tstConf.confPath = None
|
||||||
|
assert not tstConf.loadConfig()
|
||||||
|
assert not tstConf.saveConfig()
|
||||||
|
|
||||||
|
# Run again and set the paths directly and correctly
|
||||||
|
# This should create a config file as well
|
||||||
|
monkeypatch.setattr("os.path.expanduser", lambda *args: "")
|
||||||
|
tstConf.spellTool = nwConst.SP_INTERNAL
|
||||||
|
tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir)
|
||||||
|
assert tstConf.confPath == tmpDir
|
||||||
|
assert tstConf.dataPath == tmpDir
|
||||||
|
assert os.path.isfile(confFile)
|
||||||
|
|
||||||
|
copyfile(confFile, testFile)
|
||||||
|
assert cmpFiles(testFile, compFile, [2, 9])
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load and save with OSError
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
|
||||||
|
assert not tstConf.loadConfig()
|
||||||
|
assert tstConf.hasError is True
|
||||||
|
assert tstConf.errData != []
|
||||||
|
assert tstConf.getErrData().startswith("Could not")
|
||||||
|
assert tstConf.hasError is False
|
||||||
|
assert tstConf.errData == []
|
||||||
|
|
||||||
|
assert not tstConf.saveConfig()
|
||||||
|
assert tstConf.hasError is True
|
||||||
|
assert tstConf.errData != []
|
||||||
|
assert tstConf.getErrData().startswith("Could not")
|
||||||
|
assert tstConf.hasError is False
|
||||||
|
assert tstConf.errData == []
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
assert tstConf.loadConfig()
|
||||||
|
assert tstConf.saveConfig()
|
||||||
|
|
||||||
|
copyfile(confFile, testFile)
|
||||||
|
assert cmpFiles(testFile, compFile, [2, 9])
|
||||||
|
|
||||||
|
# END Test testBaseConfig_Init
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir):
|
||||||
|
"""Test recent cache file.
|
||||||
|
"""
|
||||||
|
# Check failing
|
||||||
|
tmpConf.dataPath = None
|
||||||
|
assert not tmpConf.loadRecentCache()
|
||||||
|
assert not tmpConf.saveRecentCache()
|
||||||
|
tmpConf.dataPath = tmpDir
|
||||||
|
|
||||||
|
# Add a couple of values
|
||||||
|
pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE)
|
||||||
|
pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE)
|
||||||
|
assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000)
|
||||||
|
assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600)
|
||||||
|
assert tmpConf.recentProj == {
|
||||||
|
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||||
|
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fail to Save
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
assert not tmpConf.saveRecentCache()
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Save Proper
|
||||||
|
cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE)
|
||||||
|
assert tmpConf.saveRecentCache()
|
||||||
|
assert tmpConf.saveRecentCache()
|
||||||
|
assert os.path.isfile(cacheFile)
|
||||||
|
|
||||||
|
# Fail to Load
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
tmpConf.recentProj = {}
|
||||||
|
assert not tmpConf.loadRecentCache()
|
||||||
|
assert tmpConf.recentProj == {}
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load Proper
|
||||||
|
tmpConf.recentProj = {}
|
||||||
|
assert tmpConf.loadRecentCache()
|
||||||
|
assert tmpConf.recentProj == {
|
||||||
|
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||||
|
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove Non-Existent Entry
|
||||||
|
assert not tmpConf.removeFromRecentCache("dummy")
|
||||||
|
assert tmpConf.recentProj == {
|
||||||
|
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||||
|
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove Second Entry
|
||||||
|
assert tmpConf.removeFromRecentCache(pathTwo)
|
||||||
|
assert tmpConf.recentProj == {
|
||||||
|
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||||
|
}
|
||||||
|
|
||||||
|
# END Test testBaseConfig_RecentCache
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_SetPath(tmpConf, tmpDir):
|
||||||
|
"""Test path setters.
|
||||||
|
"""
|
||||||
|
# Conf Path
|
||||||
|
assert tmpConf.setConfPath(None)
|
||||||
|
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
||||||
|
assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf"))
|
||||||
|
assert tmpConf.confPath == tmpDir
|
||||||
|
assert tmpConf.confFile == "novelwriter.conf"
|
||||||
|
assert not tmpConf.confChanged
|
||||||
|
|
||||||
|
# Data Path
|
||||||
|
assert tmpConf.setDataPath(None)
|
||||||
|
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
||||||
|
assert tmpConf.setDataPath(tmpDir)
|
||||||
|
assert tmpConf.dataPath == tmpDir
|
||||||
|
assert not tmpConf.confChanged
|
||||||
|
|
||||||
|
# Last Path
|
||||||
|
assert tmpConf.setLastPath(None)
|
||||||
|
assert tmpConf.lastPath == ""
|
||||||
|
|
||||||
|
assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp"))
|
||||||
|
assert tmpConf.lastPath == tmpDir
|
||||||
|
|
||||||
|
assert tmpConf.setLastPath("")
|
||||||
|
assert tmpConf.lastPath == ""
|
||||||
|
|
||||||
|
# END Test testBaseConfig_SetPath
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
|
||||||
|
"""Set various sizes and positions
|
||||||
|
"""
|
||||||
|
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||||
|
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
|
||||||
|
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
|
||||||
|
|
||||||
|
# GUI Scaling
|
||||||
|
# ===========
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.pxInt(10) == 10
|
||||||
|
assert tmpConf.pxInt(13) == 13
|
||||||
|
assert tmpConf.rpxInt(10) == 10
|
||||||
|
assert tmpConf.rpxInt(13) == 13
|
||||||
|
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.pxInt(10) == 20
|
||||||
|
assert tmpConf.pxInt(13) == 26
|
||||||
|
assert tmpConf.rpxInt(10) == 5
|
||||||
|
assert tmpConf.rpxInt(13) == 6
|
||||||
|
|
||||||
|
# Setter + Getter Combos
|
||||||
|
# ======================
|
||||||
|
|
||||||
|
# Window Size
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setWinSize(1205, 655)
|
||||||
|
assert not tmpConf.confChanged
|
||||||
|
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setWinSize(70, 70)
|
||||||
|
assert tmpConf.getWinSize() == [70, 70]
|
||||||
|
assert tmpConf.winGeometry == [35, 35]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setWinSize(70, 70)
|
||||||
|
assert tmpConf.getWinSize() == [70, 70]
|
||||||
|
assert tmpConf.winGeometry == [70, 70]
|
||||||
|
|
||||||
|
assert tmpConf.setWinSize(1200, 650)
|
||||||
|
|
||||||
|
# Project Tree Columns
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||||
|
assert tmpConf.getTreeColWidths() == [10, 20, 24]
|
||||||
|
assert tmpConf.treeColWidth == [5, 10, 12]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||||
|
assert tmpConf.getTreeColWidths() == [10, 20, 25]
|
||||||
|
assert tmpConf.treeColWidth == [10, 20, 25]
|
||||||
|
|
||||||
|
assert tmpConf.setTreeColWidths([200, 50, 30])
|
||||||
|
|
||||||
|
# Project Settings Tree Columns
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||||
|
assert tmpConf.getProjColWidths() == [10, 20, 30]
|
||||||
|
assert tmpConf.projColWidth == [5, 10, 15]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||||
|
assert tmpConf.getProjColWidths() == [10, 20, 30]
|
||||||
|
assert tmpConf.projColWidth == [10, 20, 30]
|
||||||
|
|
||||||
|
assert tmpConf.setProjColWidths([200, 60, 140])
|
||||||
|
|
||||||
|
# Main Pane Splitter
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setMainPanePos([200, 700])
|
||||||
|
assert tmpConf.getMainPanePos() == [200, 700]
|
||||||
|
assert tmpConf.mainPanePos == [100, 350]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setMainPanePos([200, 700])
|
||||||
|
assert tmpConf.getMainPanePos() == [200, 700]
|
||||||
|
assert tmpConf.mainPanePos == [200, 700]
|
||||||
|
|
||||||
|
assert tmpConf.setMainPanePos([300, 800])
|
||||||
|
|
||||||
|
# Doc Pane Splitter
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setDocPanePos([300, 300])
|
||||||
|
assert tmpConf.getDocPanePos() == [300, 300]
|
||||||
|
assert tmpConf.docPanePos == [150, 150]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setDocPanePos([300, 300])
|
||||||
|
assert tmpConf.getDocPanePos() == [300, 300]
|
||||||
|
assert tmpConf.docPanePos == [300, 300]
|
||||||
|
|
||||||
|
assert tmpConf.setDocPanePos([400, 400])
|
||||||
|
|
||||||
|
# View Pane Splitter
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setViewPanePos([400, 250])
|
||||||
|
assert tmpConf.getViewPanePos() == [400, 250]
|
||||||
|
assert tmpConf.viewPanePos == [200, 125]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setViewPanePos([400, 250])
|
||||||
|
assert tmpConf.getViewPanePos() == [400, 250]
|
||||||
|
assert tmpConf.viewPanePos == [400, 250]
|
||||||
|
|
||||||
|
assert tmpConf.setViewPanePos([500, 150])
|
||||||
|
|
||||||
|
# Outline Pane Splitter
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.setOutlinePanePos([400, 250])
|
||||||
|
assert tmpConf.getOutlinePanePos() == [400, 250]
|
||||||
|
assert tmpConf.outlnPanePos == [200, 125]
|
||||||
|
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.setOutlinePanePos([400, 250])
|
||||||
|
assert tmpConf.getOutlinePanePos() == [400, 250]
|
||||||
|
assert tmpConf.outlnPanePos == [400, 250]
|
||||||
|
|
||||||
|
assert tmpConf.setOutlinePanePos([500, 150])
|
||||||
|
|
||||||
|
# Getters Only
|
||||||
|
# ============
|
||||||
|
tmpConf.guiScale = 1.0
|
||||||
|
assert tmpConf.getTextWidth() == 600
|
||||||
|
assert tmpConf.getTextMargin() == 40
|
||||||
|
assert tmpConf.getTabWidth() == 40
|
||||||
|
assert tmpConf.getFocusWidth() == 800
|
||||||
|
|
||||||
|
tmpConf.guiScale = 2.0
|
||||||
|
assert tmpConf.getTextWidth() == 1200
|
||||||
|
assert tmpConf.getTextMargin() == 80
|
||||||
|
assert tmpConf.getTabWidth() == 80
|
||||||
|
assert tmpConf.getFocusWidth() == 1600
|
||||||
|
|
||||||
|
# Flag Setters
|
||||||
|
# ============
|
||||||
|
assert not tmpConf.setShowRefPanel(False)
|
||||||
|
assert not tmpConf.showRefPanel
|
||||||
|
assert tmpConf.setShowRefPanel(True)
|
||||||
|
|
||||||
|
assert not tmpConf.setViewComments(False)
|
||||||
|
assert not tmpConf.viewComments
|
||||||
|
assert tmpConf.setViewComments(True)
|
||||||
|
|
||||||
|
assert not tmpConf.setViewSynopsis(False)
|
||||||
|
assert not tmpConf.viewSynopsis
|
||||||
|
assert tmpConf.setViewSynopsis(True)
|
||||||
|
|
||||||
|
# Check Final File
|
||||||
|
# ================
|
||||||
|
|
||||||
|
assert tmpConf.confChanged
|
||||||
|
assert tmpConf.saveConfig()
|
||||||
|
assert not tmpConf.confChanged
|
||||||
|
|
||||||
|
copyfile(confFile, testFile)
|
||||||
|
assert cmpFiles(testFile, compFile, [2, 9])
|
||||||
|
|
||||||
|
# END Test testBaseConfig_SettersGetters
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseConfig_Internal(monkeypatch, tmpConf):
|
||||||
|
"""Check internal functions.
|
||||||
|
"""
|
||||||
|
# Function _packList
|
||||||
|
assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False"
|
||||||
|
|
||||||
|
# Function _unpackList
|
||||||
|
assert tmpConf._unpackList("1, 2, 3", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 3]
|
||||||
|
assert tmpConf._unpackList("1, 2 ", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 0]
|
||||||
|
assert tmpConf._unpackList("A, B, C", [0, 0, 0], tmpConf.CNF_I_LST) == [0, 0, 0]
|
||||||
|
assert tmpConf._unpackList("1, 2, 3", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["1", "2", "3"]
|
||||||
|
assert tmpConf._unpackList("A, B ", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "Z"]
|
||||||
|
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "C"]
|
||||||
|
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_STR) == ["X", "Y", "Z"]
|
||||||
|
|
||||||
|
# Function _parseLine
|
||||||
|
cnfParse = configparser.ConfigParser()
|
||||||
|
cnfParse.read_string(
|
||||||
|
"[Main]\n"
|
||||||
|
"val_string = dummy\n"
|
||||||
|
"val_int = 123\n"
|
||||||
|
"val_bool = True\n"
|
||||||
|
"val_list_string = A, B, C\n"
|
||||||
|
"val_list_int = 1, 2, 3\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_string", tmpConf.CNF_STR, "default"
|
||||||
|
) == "dummy"
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "nope", tmpConf.CNF_STR, "default"
|
||||||
|
) == "default"
|
||||||
|
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_int", tmpConf.CNF_INT, "0"
|
||||||
|
) == 123
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "nope", tmpConf.CNF_INT, 0
|
||||||
|
) == 0
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_string", tmpConf.CNF_INT, 0
|
||||||
|
) == 0
|
||||||
|
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_bool", tmpConf.CNF_BOOL, False
|
||||||
|
) is True
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "nope", tmpConf.CNF_BOOL, False
|
||||||
|
) is False
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_string", tmpConf.CNF_BOOL, False
|
||||||
|
) is False
|
||||||
|
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_list_string", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
|
||||||
|
) == ["A", "B", "C", "Z"]
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
|
||||||
|
) == ["W", "X", "Y", "Z"]
|
||||||
|
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "val_list_int", tmpConf.CNF_I_LST, [6, 7, 8, 9]
|
||||||
|
) == [1, 2, 3, 9]
|
||||||
|
assert tmpConf._parseLine(
|
||||||
|
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, [6, 7, 8, 9]
|
||||||
|
) == [6, 7, 8, 9]
|
||||||
|
|
||||||
|
# Function _checkNone
|
||||||
|
assert tmpConf._checkNone(None) is None
|
||||||
|
assert tmpConf._checkNone("None") is None
|
||||||
|
assert tmpConf._checkNone("stuff") == "stuff"
|
||||||
|
|
||||||
|
# Function _checkOptionalPackages
|
||||||
|
# (Assumes enchant package exists ans is importable)
|
||||||
|
tmpConf._checkOptionalPackages()
|
||||||
|
assert tmpConf.hasEnchant is True
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, "enchant", None)
|
||||||
|
tmpConf._checkOptionalPackages()
|
||||||
|
assert tmpConf.hasEnchant is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
monkeypatch.setattr("shutil.which", lambda *args: "dummy")
|
||||||
|
tmpConf._checkOptionalPackages()
|
||||||
|
assert tmpConf.hasAssistant is True
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
monkeypatch.setattr("shutil.which", lambda *args: None)
|
||||||
|
tmpConf._checkOptionalPackages()
|
||||||
|
assert tmpConf.hasAssistant is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testBaseConfig_Internal
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Error Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import nw
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import qApp
|
||||||
|
|
||||||
|
from dummy import causeException
|
||||||
|
|
||||||
|
from nw.error import NWErrorMessage, exceptionHandler
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir):
|
||||||
|
"""Test the error dialog.
|
||||||
|
"""
|
||||||
|
qApp.closeAllWindows()
|
||||||
|
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||||
|
qtbot.addWidget(nwGUI)
|
||||||
|
nwGUI.show()
|
||||||
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
|
|
||||||
|
nwErr = NWErrorMessage(nwGUI)
|
||||||
|
qtbot.addWidget(nwErr)
|
||||||
|
nwErr.show()
|
||||||
|
|
||||||
|
# Invalid Error Message
|
||||||
|
nwErr.setMessage(Exception, "Faulty Error", 123)
|
||||||
|
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
|
||||||
|
|
||||||
|
# Valid Error Message
|
||||||
|
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", lambda: "1.2.3")
|
||||||
|
nwErr.setMessage(Exception, "Fine Error", None)
|
||||||
|
theMessage = nwErr.msgBody.toPlainText()
|
||||||
|
assert theMessage
|
||||||
|
assert "Fine Error" in theMessage
|
||||||
|
assert "Exception" in theMessage
|
||||||
|
assert "(1.2.3)" in theMessage
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# No kernel version retrieved
|
||||||
|
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", causeException)
|
||||||
|
nwErr.setMessage(Exception, "Almost Fine Error", None)
|
||||||
|
theMessage = nwErr.msgBody.toPlainText()
|
||||||
|
assert theMessage
|
||||||
|
assert "(Unknown)" in theMessage
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
nwErr._doClose()
|
||||||
|
nwErr.close()
|
||||||
|
nwGUI.closeMain()
|
||||||
|
|
||||||
|
# END Test testBaseError_Dialog
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseError_Handler(qtbot, monkeypatch, fncDir, tmpDir):
|
||||||
|
"""Test the error handler. This test doesn'thave any asserts, but it
|
||||||
|
checks that the error handler handles potential exceptions. The test
|
||||||
|
will fail if excpetions are not handled.
|
||||||
|
"""
|
||||||
|
qApp.closeAllWindows()
|
||||||
|
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||||
|
qtbot.addWidget(nwGUI)
|
||||||
|
nwGUI.show()
|
||||||
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
|
|
||||||
|
# Normal shutdown
|
||||||
|
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||||
|
exceptionHandler(Exception, "Error Message", None)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Should not crash when no GUI is found
|
||||||
|
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", lambda: [])
|
||||||
|
exceptionHandler(Exception, "Error Message", None)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Should handle qApp failing
|
||||||
|
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", causeException)
|
||||||
|
exceptionHandler(Exception, "Error Message", None)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Should handle failing to close main GUI
|
||||||
|
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||||
|
monkeypatch.setattr(nwGUI, "closeMain", causeException)
|
||||||
|
exceptionHandler(Exception, "Error Message", None)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
nwGUI.closeMain()
|
||||||
|
|
||||||
|
# END Test testBaseError_Handler
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Main Init Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import nw
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dummy import DummyMain
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseInit_Launch(caplog, monkeypatch, tmpDir):
|
||||||
|
"""Check launching the main GUI.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||||
|
|
||||||
|
# Testmode launch
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert isinstance(nwGUI, DummyMain)
|
||||||
|
|
||||||
|
# Darwin launch
|
||||||
|
monkeypatch.setitem(sys.modules, "Foundation", None)
|
||||||
|
osDarwin = nw.CONFIG.osDarwin
|
||||||
|
nw.CONFIG.osDarwin = True
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert isinstance(nwGUI, DummyMain)
|
||||||
|
assert "Foundation" in caplog.messages[1]
|
||||||
|
nw.CONFIG.osDarwin = osDarwin
|
||||||
|
|
||||||
|
# Normal launch
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
nw.main(["--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
|
|
||||||
|
assert ex.value.code == 0
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testBaseInit_Launch
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseInit_Options(monkeypatch, tmpDir):
|
||||||
|
"""Test command line options for logging level.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||||
|
monkeypatch.setattr(sys, "argv", [
|
||||||
|
"novelWriter.py", "--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir
|
||||||
|
])
|
||||||
|
|
||||||
|
# Defaults w/None Args
|
||||||
|
nwGUI = nw.main()
|
||||||
|
assert nw.logger.getEffectiveLevel() == logging.WARNING
|
||||||
|
assert nw.CONFIG.debugInfo is False
|
||||||
|
assert nw.CONFIG.showGUI is False
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "--style=Fusion"]
|
||||||
|
)
|
||||||
|
assert nw.logger.getEffectiveLevel() == logging.WARNING
|
||||||
|
assert nw.CONFIG.debugInfo is False
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
# Log Levels
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--info", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nw.logger.getEffectiveLevel() == logging.INFO
|
||||||
|
assert nw.CONFIG.debugInfo is False
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--debug", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nw.logger.getEffectiveLevel() == logging.DEBUG
|
||||||
|
assert nw.CONFIG.debugInfo is True
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--verbose", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nw.logger.getEffectiveLevel() == 5
|
||||||
|
assert nw.CONFIG.debugInfo is True
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
# Help and Version
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--help", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
assert ex.value.code == 0
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--version", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
assert ex.value.code == 0
|
||||||
|
|
||||||
|
# Invalid options
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--invalid", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
assert ex.value.code == 2
|
||||||
|
|
||||||
|
# Project Path
|
||||||
|
nwGUI = nw.main(
|
||||||
|
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "sample/"]
|
||||||
|
)
|
||||||
|
assert nw.CONFIG.cmdOpen == "sample/"
|
||||||
|
assert nwGUI.closeMain() == "closeMain"
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testBaseInit_Options
|
||||||
|
|
||||||
|
@pytest.mark.base
|
||||||
|
def testBaseInit_Imports(caplog, monkeypatch, tmpDir):
|
||||||
|
"""Check import error handling.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.__init__", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.resize", lambda *args: None)
|
||||||
|
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.showMessage", lambda *args: None)
|
||||||
|
monkeypatch.setitem(sys.modules, "lxml", None)
|
||||||
|
monkeypatch.setattr("sys.hexversion", 0x0)
|
||||||
|
monkeypatch.setattr("nw.CONFIG.verQtValue", 50000)
|
||||||
|
monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as ex:
|
||||||
|
_ = nw.main(
|
||||||
|
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ex.value.code & 4 == 4 # Python version not satisfied
|
||||||
|
assert ex.value.code & 8 == 8 # Qt version not satisfied
|
||||||
|
assert ex.value.code & 16 == 16 # PyQt version not satisfied
|
||||||
|
assert ex.value.code & 32 == 32 # lxml package missing
|
||||||
|
|
||||||
|
assert "At least Python" in caplog.messages[0]
|
||||||
|
assert "At least Qt5" in caplog.messages[1]
|
||||||
|
assert "At least PyQt5" in caplog.messages[2]
|
||||||
|
assert "lxml" in caplog.messages[3]
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testBaseInit_Imports
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""novelWriter Config Class Tester
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
|
|
||||||
from nwtools import cmpFiles
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigCore(tmpConf, nwTemp, nwRef):
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
|
||||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
|
||||||
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
assert tmpConf.saveConfig()
|
|
||||||
assert cmpFiles(testConf, refConf, [2])
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
assert tmpConf.loadConfig()
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigSetConfPath(tmpConf, nwTemp):
|
|
||||||
assert tmpConf.setConfPath(None)
|
|
||||||
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
|
||||||
assert tmpConf.setConfPath(os.path.join(nwTemp, "novelwriter.conf"))
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
assert tmpConf.confFile == "novelwriter.conf"
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigSetDataPath(tmpConf, nwTemp):
|
|
||||||
assert tmpConf.setDataPath(None)
|
|
||||||
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
|
||||||
assert tmpConf.setDataPath(nwTemp)
|
|
||||||
assert tmpConf.dataPath == nwTemp
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigSetWinSize(tmpConf, nwTemp, nwRef):
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
|
||||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
|
||||||
tmpConf.guiScale = 1.0
|
|
||||||
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
assert tmpConf.setWinSize(1205, 655)
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
assert tmpConf.setWinSize(70, 70)
|
|
||||||
assert tmpConf.confChanged
|
|
||||||
assert tmpConf.setWinSize(1200, 650)
|
|
||||||
assert tmpConf.saveConfig()
|
|
||||||
|
|
||||||
assert cmpFiles(testConf, refConf, [2])
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef):
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
|
||||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
|
||||||
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
tmpConf.guiScale = 1.0
|
|
||||||
|
|
||||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
|
||||||
assert tmpConf.treeColWidth == [10, 20, 25]
|
|
||||||
assert tmpConf.setTreeColWidths([200, 50, 30])
|
|
||||||
|
|
||||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
|
||||||
assert tmpConf.projColWidth == [10, 20, 30]
|
|
||||||
assert tmpConf.setProjColWidths([200, 60, 140])
|
|
||||||
|
|
||||||
assert tmpConf.confChanged
|
|
||||||
assert tmpConf.saveConfig()
|
|
||||||
|
|
||||||
assert cmpFiles(testConf, refConf, [2])
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigSetPanePos(tmpConf, nwTemp, nwRef):
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
|
||||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
|
||||||
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
|
|
||||||
tmpConf.guiScale = 2.0
|
|
||||||
assert tmpConf.setMainPanePos([200, 700])
|
|
||||||
assert tmpConf.mainPanePos == [100, 350]
|
|
||||||
assert tmpConf.getMainPanePos() == [200, 700]
|
|
||||||
|
|
||||||
assert tmpConf.setDocPanePos([300, 300])
|
|
||||||
assert tmpConf.docPanePos == [150, 150]
|
|
||||||
assert tmpConf.getDocPanePos() == [300, 300]
|
|
||||||
|
|
||||||
assert tmpConf.setViewPanePos([400, 250])
|
|
||||||
assert tmpConf.viewPanePos == [200, 125]
|
|
||||||
assert tmpConf.getViewPanePos() == [400, 250]
|
|
||||||
|
|
||||||
assert tmpConf.setOutlinePanePos([400, 250])
|
|
||||||
assert tmpConf.outlnPanePos == [200, 125]
|
|
||||||
assert tmpConf.getOutlinePanePos() == [400, 250]
|
|
||||||
|
|
||||||
tmpConf.guiScale = 1.0
|
|
||||||
assert tmpConf.setMainPanePos([300, 800])
|
|
||||||
assert tmpConf.setDocPanePos([400, 400])
|
|
||||||
assert tmpConf.setViewPanePos([500, 150])
|
|
||||||
assert tmpConf.setOutlinePanePos([500, 150])
|
|
||||||
|
|
||||||
assert tmpConf.confChanged
|
|
||||||
assert tmpConf.saveConfig()
|
|
||||||
|
|
||||||
assert cmpFiles(testConf, refConf, [2])
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigFlags(tmpConf, nwTemp, nwRef):
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
|
||||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
|
||||||
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
|
|
||||||
assert not tmpConf.setShowRefPanel(False)
|
|
||||||
assert tmpConf.setShowRefPanel(True)
|
|
||||||
|
|
||||||
assert not tmpConf.setViewComments(False)
|
|
||||||
assert not tmpConf.viewComments
|
|
||||||
assert tmpConf.setViewComments(True)
|
|
||||||
|
|
||||||
assert not tmpConf.setViewSynopsis(False)
|
|
||||||
assert not tmpConf.viewSynopsis
|
|
||||||
assert tmpConf.setViewSynopsis(True)
|
|
||||||
|
|
||||||
assert tmpConf.confChanged
|
|
||||||
assert tmpConf.saveConfig()
|
|
||||||
|
|
||||||
assert cmpFiles(testConf, refConf, [2])
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testTextSizes(tmpConf, nwTemp, nwRef):
|
|
||||||
assert tmpConf.confPath == nwTemp
|
|
||||||
|
|
||||||
tmpConf.guiScale = 2.0
|
|
||||||
assert tmpConf.getTextWidth() == 1200
|
|
||||||
assert tmpConf.getTextMargin() == 80
|
|
||||||
assert tmpConf.getTabWidth() == 80
|
|
||||||
assert tmpConf.getFocusWidth() == 1600
|
|
||||||
tmpConf.guiScale = 1.0
|
|
||||||
|
|
||||||
assert not tmpConf.confChanged
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigErrors(tmpConf):
|
|
||||||
nonPath = os.path.join("somewhere", "over", "the", "rainbow")
|
|
||||||
assert tmpConf.initConfig(nonPath, nonPath)
|
|
||||||
assert tmpConf.hasError
|
|
||||||
assert not tmpConf.loadConfig()
|
|
||||||
assert not tmpConf.saveConfig()
|
|
||||||
assert not tmpConf.loadRecentCache()
|
|
||||||
assert len(tmpConf.getErrData()) > 0
|
|
||||||
|
|
||||||
@pytest.mark.core
|
|
||||||
def testConfigInternals(tmpConf):
|
|
||||||
assert tmpConf._checkNone(None) is None
|
|
||||||
assert tmpConf._checkNone("None") is None
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter NWDoc Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dummy import causeOSError
|
||||||
|
|
||||||
|
from nw.core import NWProject, NWDoc
|
||||||
|
from nw.core.item import NWItem
|
||||||
|
from nw.constants import nwItemClass, nwItemLayout
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal):
|
||||||
|
"""Test loading and saving a document with the NWDoc class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
assert theProject.openProject(nwMinimal)
|
||||||
|
assert theProject.projPath == nwMinimal
|
||||||
|
|
||||||
|
theDoc = NWDoc(theProject, dummyGUI)
|
||||||
|
sHandle = "8c659a11cd429"
|
||||||
|
|
||||||
|
# Not a valid handle
|
||||||
|
assert theDoc.openDocument("dummy") is None
|
||||||
|
|
||||||
|
# Non-existent handle
|
||||||
|
assert theDoc.openDocument("0000000000000") is None
|
||||||
|
|
||||||
|
# Cause open() to fail while loading
|
||||||
|
def dummyOpen(*args, **kwargs):
|
||||||
|
raise OSError
|
||||||
|
|
||||||
|
monkeypatch.setattr("builtins.open", dummyOpen)
|
||||||
|
assert theDoc.openDocument(sHandle) is None
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load the text
|
||||||
|
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
|
||||||
|
|
||||||
|
# Try to open a new (non-existent) file
|
||||||
|
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
|
||||||
|
assert nHandle is not None
|
||||||
|
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
|
||||||
|
assert theDoc.openDocument(xHandle) == ""
|
||||||
|
|
||||||
|
# Check cached item
|
||||||
|
assert isinstance(theDoc._theItem, NWItem)
|
||||||
|
assert theDoc.openDocument(xHandle, isOrphan=True) == ""
|
||||||
|
assert theDoc._theItem is None
|
||||||
|
|
||||||
|
# Set handle and save again
|
||||||
|
theText = "### Test File\n\nText ...\n\n"
|
||||||
|
assert theDoc.openDocument(xHandle) == ""
|
||||||
|
assert theDoc.saveDocument(theText)
|
||||||
|
|
||||||
|
# Save again to ensure temp file and previous file is handled
|
||||||
|
assert theDoc.saveDocument(theText)
|
||||||
|
|
||||||
|
# Check file content
|
||||||
|
docPath = os.path.join(nwMinimal, "content", xHandle+".nwd")
|
||||||
|
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||||
|
assert inFile.read() == (
|
||||||
|
"%%~name: New File\n"
|
||||||
|
f"%%~path: a508bb932959c/{xHandle}\n"
|
||||||
|
"%%~kind: NOVEL/SCENE\n"
|
||||||
|
"### Test File\n\n"
|
||||||
|
"Text ...\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Force no meta data
|
||||||
|
theDoc._theItem = None
|
||||||
|
assert theDoc.saveDocument(theText)
|
||||||
|
|
||||||
|
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||||
|
assert inFile.read() == theText
|
||||||
|
|
||||||
|
# Cause open() to fail while saving
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
assert not theDoc.saveDocument(theText)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Saving with no handle
|
||||||
|
theDoc.clearDocument()
|
||||||
|
assert not theDoc.saveDocument(theText)
|
||||||
|
|
||||||
|
# Delete the last document
|
||||||
|
assert not theDoc.deleteDocument("dummy")
|
||||||
|
assert os.path.isfile(docPath)
|
||||||
|
|
||||||
|
# Cause the delete to fail
|
||||||
|
monkeypatch.setattr("os.unlink", causeOSError)
|
||||||
|
assert not theDoc.deleteDocument(xHandle)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Make the delete pass
|
||||||
|
assert theDoc.deleteDocument(xHandle)
|
||||||
|
assert not os.path.isfile(docPath)
|
||||||
|
|
||||||
|
# END Test testCoreDocument_Load
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal):
|
||||||
|
"""Test other methods of the NWDoc class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
assert theProject.openProject(nwMinimal)
|
||||||
|
assert theProject.projPath == nwMinimal
|
||||||
|
|
||||||
|
theDoc = NWDoc(theProject, dummyGUI)
|
||||||
|
sHandle = "8c659a11cd429"
|
||||||
|
docPath = os.path.join(nwMinimal, "content", sHandle+".nwd")
|
||||||
|
|
||||||
|
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
|
||||||
|
|
||||||
|
# Check location
|
||||||
|
assert theDoc.getFileLocation() == docPath
|
||||||
|
|
||||||
|
# Check the item
|
||||||
|
assert theDoc.getCurrentItem() is not None
|
||||||
|
assert theDoc.getCurrentItem().itemHandle == sHandle
|
||||||
|
|
||||||
|
# Check the meta
|
||||||
|
theName, theParent, theClass, theLayout = theDoc.getMeta()
|
||||||
|
assert theName == "New Scene"
|
||||||
|
assert theParent == "a6d311a93600a"
|
||||||
|
assert theClass == nwItemClass.NOVEL
|
||||||
|
assert theLayout == nwItemLayout.SCENE
|
||||||
|
|
||||||
|
# Add meta data garbage
|
||||||
|
assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n")
|
||||||
|
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||||
|
assert inFile.read() == (
|
||||||
|
"%%~name: New Scene\n"
|
||||||
|
f"%%~path: a6d311a93600a/{sHandle}\n"
|
||||||
|
"%%~kind: NOVEL/SCENE\n"
|
||||||
|
"%%~ stuff\n"
|
||||||
|
"### Test File\n\n"
|
||||||
|
"Text ...\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert theDoc.openDocument(sHandle) == "### Test File\n\nText ...\n\n"
|
||||||
|
|
||||||
|
# END Test testCoreDocument_Methods
|
||||||
@@ -8,28 +8,28 @@ import json
|
|||||||
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from nwtools import cmpFiles
|
from tools import cmpFiles
|
||||||
|
|
||||||
from nw.core.project import NWProject
|
from nw.core.project import NWProject
|
||||||
from nw.core.index import NWIndex
|
from nw.core.index import NWIndex
|
||||||
from nw.constants import nwItemClass, nwItemLayout
|
from nw.constants import nwItemClass, nwItemLayout
|
||||||
|
|
||||||
@pytest.mark.project
|
@pytest.mark.core
|
||||||
def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
|
def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
|
||||||
"""Test core functionality of scaning, saving, loading and checking
|
"""Test core functionality of scaning, saving, loading and checking
|
||||||
the index cache file.
|
the index cache file.
|
||||||
"""
|
"""
|
||||||
projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
|
projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
|
||||||
testFile = os.path.join(nwTempProj, "1_tagsIndex.json")
|
testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
|
||||||
refFile = os.path.join(nwRef, "proj", "1_tagsIndex.json")
|
compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
theProject = NWProject(dummyGUI)
|
||||||
theProject.projTree.setSeed(42)
|
theProject.projTree.setSeed(42)
|
||||||
assert theProject.openProject(nwLipsum)
|
assert theProject.openProject(nwLipsum)
|
||||||
|
|
||||||
monkeypatch.setattr("nw.core.index.time", lambda: 123.4)
|
monkeypatch.setattr("nw.core.index.time", lambda: 123.4)
|
||||||
|
|
||||||
theIndex = NWIndex(theProject, nwDummy)
|
theIndex = NWIndex(theProject, dummyGUI)
|
||||||
notIndexable = {
|
notIndexable = {
|
||||||
"b3643d0f92e32": False, # Novel ROOT
|
"b3643d0f92e32": False, # Novel ROOT
|
||||||
"45e6b01ca35c1": False, # Chapter One FOLDER
|
"45e6b01ca35c1": False, # Chapter One FOLDER
|
||||||
@@ -136,17 +136,19 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
|
|||||||
assert theProject.closeProject()
|
assert theProject.closeProject()
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, compFile)
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreIndex_LoadSave
|
||||||
def testIndexScanThis(nwMinimal, nwDummy):
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreIndex_ScanThis(nwMinimal, dummyGUI):
|
||||||
"""Test the tag scanner function scanThis.
|
"""Test the tag scanner function scanThis.
|
||||||
"""
|
"""
|
||||||
theProject = NWProject(nwDummy)
|
theProject = NWProject(dummyGUI)
|
||||||
theProject.projTree.setSeed(42)
|
theProject.projTree.setSeed(42)
|
||||||
assert theProject.openProject(nwMinimal)
|
assert theProject.openProject(nwMinimal)
|
||||||
|
|
||||||
theIndex = NWIndex(theProject, nwDummy)
|
theIndex = NWIndex(theProject, dummyGUI)
|
||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
|
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
|
||||||
assert not isValid
|
assert not isValid
|
||||||
@@ -162,40 +164,42 @@ def testIndexScanThis(nwMinimal, nwDummy):
|
|||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("@a:")
|
isValid, theBits, thePos = theIndex.scanThis("@a:")
|
||||||
assert isValid
|
assert isValid
|
||||||
assert str(theBits) == "['@a']"
|
assert theBits == ["@a"]
|
||||||
assert str(thePos) == "[0]"
|
assert thePos == [0]
|
||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("@a:b")
|
isValid, theBits, thePos = theIndex.scanThis("@a:b")
|
||||||
assert isValid
|
assert isValid
|
||||||
assert str(theBits) == "['@a', 'b']"
|
assert theBits == ["@a", "b"]
|
||||||
assert str(thePos) == "[0, 3]"
|
assert thePos == [0, 3]
|
||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
|
isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
|
||||||
assert isValid
|
assert isValid
|
||||||
assert str(theBits) == "['@a', 'b', 'c', 'd']"
|
assert theBits == ["@a", "b", "c", "d"]
|
||||||
assert str(thePos) == "[0, 3, 5, 7]"
|
assert thePos == [0, 3, 5, 7]
|
||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d")
|
isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d")
|
||||||
assert isValid
|
assert isValid
|
||||||
assert str(theBits) == "['@a', 'b', 'c', 'd']"
|
assert theBits == ["@a", "b", "c", "d"]
|
||||||
assert str(thePos) == "[0, 5, 9, 13]"
|
assert thePos == [0, 5, 9, 13]
|
||||||
|
|
||||||
isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
|
isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
|
||||||
assert isValid
|
assert isValid
|
||||||
assert str(theBits) == "['@tag', 'this', 'and this']"
|
assert theBits == ["@tag", "this", "and this"]
|
||||||
assert str(thePos) == "[0, 6, 12]"
|
assert thePos == [0, 6, 12]
|
||||||
|
|
||||||
assert theProject.closeProject()
|
assert theProject.closeProject()
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreIndex_ScanThis
|
||||||
def testIndexCheckThese(nwMinimal, nwDummy):
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
|
||||||
"""Test the tag checker function checkThese.
|
"""Test the tag checker function checkThese.
|
||||||
"""
|
"""
|
||||||
theProject = NWProject(nwDummy)
|
theProject = NWProject(dummyGUI)
|
||||||
theProject.projTree.setSeed(42)
|
theProject.projTree.setSeed(42)
|
||||||
assert theProject.openProject(nwMinimal)
|
assert theProject.openProject(nwMinimal)
|
||||||
|
|
||||||
theIndex = NWIndex(theProject, nwDummy)
|
theIndex = NWIndex(theProject, dummyGUI)
|
||||||
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
||||||
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||||
nItem = theProject.projTree[nHandle]
|
nItem = theProject.projTree[nHandle]
|
||||||
@@ -209,29 +213,32 @@ def testIndexCheckThese(nwMinimal, nwDummy):
|
|||||||
"# Hello World!\n"
|
"# Hello World!\n"
|
||||||
"@pov: Jane"
|
"@pov: Jane"
|
||||||
))
|
))
|
||||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
|
assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
|
||||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
||||||
|
|
||||||
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
|
assert theIndex.checkThese([], cItem) == []
|
||||||
assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]"
|
assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True]
|
||||||
assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]"
|
assert theIndex.checkThese(["@tag", "John"], cItem) == [True, True]
|
||||||
assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]"
|
assert theIndex.checkThese(["@tag", "Jane"], nItem) == [True, False]
|
||||||
assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]"
|
assert theIndex.checkThese(["@tag", "John"], nItem) == [True, True]
|
||||||
assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]"
|
assert theIndex.checkThese(["@pov", "John"], nItem) == [True, False]
|
||||||
assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]"
|
assert theIndex.checkThese(["@pov", "Jane"], nItem) == [True, True]
|
||||||
assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]"
|
assert theIndex.checkThese(["@ pov", "Jane"], nItem) == [False, False]
|
||||||
|
assert theIndex.checkThese(["@what", "Jane"], nItem) == [False, False]
|
||||||
|
|
||||||
assert theProject.closeProject()
|
assert theProject.closeProject()
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreIndex_CheckThese
|
||||||
def testIndexScanText(nwMinimal, nwDummy):
|
|
||||||
"""Check the index data extraction functions.
|
@pytest.mark.core
|
||||||
|
def testCoreIndex_ScanText(nwMinimal, dummyGUI):
|
||||||
|
"""Check the index text scanner.
|
||||||
"""
|
"""
|
||||||
theProject = NWProject(nwDummy)
|
theProject = NWProject(dummyGUI)
|
||||||
theProject.projTree.setSeed(42)
|
theProject.projTree.setSeed(42)
|
||||||
assert theProject.openProject(nwMinimal)
|
assert theProject.openProject(nwMinimal)
|
||||||
|
|
||||||
theIndex = NWIndex(theProject, nwDummy)
|
theIndex = NWIndex(theProject, dummyGUI)
|
||||||
|
|
||||||
# Some items for fail to scan tests
|
# Some items for fail to scan tests
|
||||||
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
|
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
|
||||||
@@ -392,21 +399,23 @@ def testIndexScanText(nwMinimal, nwDummy):
|
|||||||
"% synopsis: Synopsis One.\n\n"
|
"% synopsis: Synopsis One.\n\n"
|
||||||
"Paragraph One.\n\n"
|
"Paragraph One.\n\n"
|
||||||
))
|
))
|
||||||
assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == (
|
assert theIndex.refIndex[sHandle]["T000001"]["tags"] == (
|
||||||
"[[3, '@pov', 'One'], [5, '@char', 'Two']]"
|
[[3, "@pov", "One"], [5, "@char", "Two"]]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert theProject.closeProject()
|
assert theProject.closeProject()
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreIndex_ScanText
|
||||||
def testIndexExtractData(nwMinimal, nwDummy):
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
|
||||||
"""Check the index data extraction functions.
|
"""Check the index data extraction functions.
|
||||||
"""
|
"""
|
||||||
theProject = NWProject(nwDummy)
|
theProject = NWProject(dummyGUI)
|
||||||
theProject.projTree.setSeed(42)
|
theProject.projTree.setSeed(42)
|
||||||
assert theProject.openProject(nwMinimal)
|
assert theProject.openProject(nwMinimal)
|
||||||
|
|
||||||
theIndex = NWIndex(theProject, nwDummy)
|
theIndex = NWIndex(theProject, dummyGUI)
|
||||||
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
||||||
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||||
|
|
||||||
@@ -424,7 +433,13 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# The novel structure should contain the pointer to the novel file header
|
# The novel structure should contain the pointer to the novel file header
|
||||||
assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle
|
assert theIndex.getNovelStructure() == ["%s:T000001" % nHandle]
|
||||||
|
|
||||||
|
# Check that excluded files can be skipped
|
||||||
|
theProject.projTree[nHandle].setExported(False)
|
||||||
|
assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle]
|
||||||
|
assert theIndex.getNovelStructure(skipExcluded=True) == []
|
||||||
|
assert theIndex.getNovelStructure() == []
|
||||||
|
|
||||||
# The novel file should have the correct counts
|
# The novel file should have the correct counts
|
||||||
cC, wC, pC = theIndex.getCounts(nHandle)
|
cC, wC, pC = theIndex.getCounts(nHandle)
|
||||||
@@ -443,8 +458,8 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
|||||||
|
|
||||||
# The novel file should now refer to Jane as @pov and @char
|
# The novel file should now refer to Jane as @pov and @char
|
||||||
theRefs = theIndex.getReferences(nHandle)
|
theRefs = theIndex.getReferences(nHandle)
|
||||||
assert str(theRefs["@pov"]) == "['Jane']"
|
assert theRefs["@pov"] == ["Jane"]
|
||||||
assert str(theRefs["@char"]) == "['Jane']"
|
assert theRefs["@char"] == ["Jane"]
|
||||||
|
|
||||||
##
|
##
|
||||||
# getBackReferenceList
|
# getBackReferenceList
|
||||||
@@ -455,7 +470,7 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
|||||||
|
|
||||||
# The character file should have a record of the reference from the novel file
|
# The character file should have a record of the reference from the novel file
|
||||||
theRefs = theIndex.getBackReferenceList(cHandle)
|
theRefs = theIndex.getBackReferenceList(cHandle)
|
||||||
assert str(theRefs) == "{'%s': 'T000001'}" % nHandle
|
assert theRefs == {nHandle: "T000001"}
|
||||||
|
|
||||||
##
|
##
|
||||||
# getTagSource
|
# getTagSource
|
||||||
@@ -535,3 +550,5 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
|||||||
assert pC == 2
|
assert pC == 2
|
||||||
|
|
||||||
assert theProject.closeProject()
|
assert theProject.closeProject()
|
||||||
|
|
||||||
|
# END Test testCoreIndex_ExtractData
|
||||||
@@ -6,13 +6,15 @@ import pytest
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from nw.core.project import NWProject, NWItem
|
from nw.core import NWProject
|
||||||
|
from nw.core.item import NWItem
|
||||||
from nw.constants import nwItemClass, nwItemType, nwItemLayout
|
from nw.constants import nwItemClass, nwItemType, nwItemLayout
|
||||||
|
|
||||||
@pytest.mark.project
|
@pytest.mark.core
|
||||||
def testItemSettersSimple(nwDummy):
|
def testCoreItem_Setters(dummyGUI):
|
||||||
|
"""Test all the simple setters for the NWItem class.
|
||||||
theProject = NWProject(nwDummy)
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
theItem = NWItem(theProject)
|
theItem = NWItem(theProject)
|
||||||
|
|
||||||
# Name
|
# Name
|
||||||
@@ -20,12 +22,16 @@ def testItemSettersSimple(nwDummy):
|
|||||||
assert theItem.itemName == "A Name"
|
assert theItem.itemName == "A Name"
|
||||||
theItem.setName("\t A Name ")
|
theItem.setName("\t A Name ")
|
||||||
assert theItem.itemName == "A Name"
|
assert theItem.itemName == "A Name"
|
||||||
|
theItem.setName(123)
|
||||||
|
assert theItem.itemName == ""
|
||||||
|
|
||||||
# Handle
|
# Handle
|
||||||
theItem.setHandle(123)
|
theItem.setHandle(123)
|
||||||
assert theItem.itemHandle is None
|
assert theItem.itemHandle is None
|
||||||
theItem.setHandle("0123456789abcdef")
|
theItem.setHandle("0123456789abcdef")
|
||||||
assert theItem.itemHandle is None
|
assert theItem.itemHandle is None
|
||||||
|
theItem.setHandle("0123456789abg")
|
||||||
|
assert theItem.itemHandle is None
|
||||||
theItem.setHandle("0123456789abc")
|
theItem.setHandle("0123456789abc")
|
||||||
assert theItem.itemHandle == "0123456789abc"
|
assert theItem.itemHandle == "0123456789abc"
|
||||||
|
|
||||||
@@ -36,6 +42,8 @@ def testItemSettersSimple(nwDummy):
|
|||||||
assert theItem.itemParent is None
|
assert theItem.itemParent is None
|
||||||
theItem.setParent("0123456789abcdef")
|
theItem.setParent("0123456789abcdef")
|
||||||
assert theItem.itemParent is None
|
assert theItem.itemParent is None
|
||||||
|
theItem.setParent("0123456789abg")
|
||||||
|
assert theItem.itemParent is None
|
||||||
theItem.setParent("0123456789abc")
|
theItem.setParent("0123456789abc")
|
||||||
assert theItem.itemParent == "0123456789abc"
|
assert theItem.itemParent == "0123456789abc"
|
||||||
|
|
||||||
@@ -59,6 +67,19 @@ def testItemSettersSimple(nwDummy):
|
|||||||
theItem.setStatus("Main")
|
theItem.setStatus("Main")
|
||||||
assert theItem.itemStatus == "Main"
|
assert theItem.itemStatus == "Main"
|
||||||
|
|
||||||
|
# Importance
|
||||||
|
theItem.itemClass = nwItemClass.NOVEL
|
||||||
|
theItem.setStatus("Nonsense")
|
||||||
|
assert theItem.itemStatus == "New"
|
||||||
|
theItem.setStatus("New")
|
||||||
|
assert theItem.itemStatus == "New"
|
||||||
|
theItem.setStatus("Note")
|
||||||
|
assert theItem.itemStatus == "Note"
|
||||||
|
theItem.setStatus("Draft")
|
||||||
|
assert theItem.itemStatus == "Draft"
|
||||||
|
theItem.setStatus("Finished")
|
||||||
|
assert theItem.itemStatus == "Finished"
|
||||||
|
|
||||||
# Expanded
|
# Expanded
|
||||||
theItem.setExpanded(8)
|
theItem.setExpanded(8)
|
||||||
assert not theItem.isExpanded
|
assert not theItem.isExpanded
|
||||||
@@ -73,6 +94,20 @@ def testItemSettersSimple(nwDummy):
|
|||||||
theItem.setExpanded(True)
|
theItem.setExpanded(True)
|
||||||
assert theItem.isExpanded
|
assert theItem.isExpanded
|
||||||
|
|
||||||
|
# Exported
|
||||||
|
theItem.setExported(8)
|
||||||
|
assert not theItem.isExported
|
||||||
|
theItem.setExported(None)
|
||||||
|
assert not theItem.isExported
|
||||||
|
theItem.setExported("None")
|
||||||
|
assert not theItem.isExported
|
||||||
|
theItem.setExported("What?")
|
||||||
|
assert not theItem.isExported
|
||||||
|
theItem.setExported("True")
|
||||||
|
assert theItem.isExported
|
||||||
|
theItem.setExported(True)
|
||||||
|
assert theItem.isExported
|
||||||
|
|
||||||
# CharCount
|
# CharCount
|
||||||
theItem.setCharCount(None)
|
theItem.setCharCount(None)
|
||||||
assert theItem.charCount == 0
|
assert theItem.charCount == 0
|
||||||
@@ -105,10 +140,47 @@ def testItemSettersSimple(nwDummy):
|
|||||||
theItem.setCursorPos(1)
|
theItem.setCursorPos(1)
|
||||||
assert theItem.cursorPos == 1
|
assert theItem.cursorPos == 1
|
||||||
|
|
||||||
@pytest.mark.project
|
# Initial Count
|
||||||
def testItemClassSetter(nwDummy):
|
theItem.setWordCount(234)
|
||||||
|
theItem.saveInitialCount()
|
||||||
|
assert theItem.initCount == 234
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
# END Test testCoreItem_Setters
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreItem_TypeSetter(dummyGUI):
|
||||||
|
"""Test the setter for all the nwItemType values for the NWItem
|
||||||
|
class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theItem = NWItem(theProject)
|
||||||
|
|
||||||
|
# Type
|
||||||
|
theItem.setType(None)
|
||||||
|
assert theItem.itemType == nwItemType.NO_TYPE
|
||||||
|
theItem.setType("NONSENSE")
|
||||||
|
assert theItem.itemType == nwItemType.NO_TYPE
|
||||||
|
theItem.setType("NO_TYPE")
|
||||||
|
assert theItem.itemType == nwItemType.NO_TYPE
|
||||||
|
theItem.setType("ROOT")
|
||||||
|
assert theItem.itemType == nwItemType.ROOT
|
||||||
|
theItem.setType("FOLDER")
|
||||||
|
assert theItem.itemType == nwItemType.FOLDER
|
||||||
|
theItem.setType("FILE")
|
||||||
|
assert theItem.itemType == nwItemType.FILE
|
||||||
|
theItem.setType("TRASH")
|
||||||
|
assert theItem.itemType == nwItemType.TRASH
|
||||||
|
theItem.setType(nwItemType.ROOT)
|
||||||
|
assert theItem.itemType == nwItemType.ROOT
|
||||||
|
|
||||||
|
# END Test testCoreItem_TypeSetter
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreItem_ClassSetter(dummyGUI):
|
||||||
|
"""Test the setter for all the nwItemClass values for the NWItem
|
||||||
|
class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
theItem = NWItem(theProject)
|
theItem = NWItem(theProject)
|
||||||
|
|
||||||
# Class
|
# Class
|
||||||
@@ -138,33 +210,17 @@ def testItemClassSetter(nwDummy):
|
|||||||
assert theItem.itemClass == nwItemClass.ARCHIVE
|
assert theItem.itemClass == nwItemClass.ARCHIVE
|
||||||
theItem.setClass("TRASH")
|
theItem.setClass("TRASH")
|
||||||
assert theItem.itemClass == nwItemClass.TRASH
|
assert theItem.itemClass == nwItemClass.TRASH
|
||||||
|
theItem.setClass(nwItemClass.NOVEL)
|
||||||
|
assert theItem.itemClass == nwItemClass.NOVEL
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreItem_ClassSetter
|
||||||
def testItemTypeSetter(nwDummy):
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
@pytest.mark.core
|
||||||
theItem = NWItem(theProject)
|
def testCoreItem_LayoutSetter(dummyGUI):
|
||||||
|
"""Test the setter for all the nwItemLayout values for the NWItem
|
||||||
# Type
|
class.
|
||||||
theItem.setType(None)
|
"""
|
||||||
assert theItem.itemType == nwItemType.NO_TYPE
|
theProject = NWProject(dummyGUI)
|
||||||
theItem.setType("NONSENSE")
|
|
||||||
assert theItem.itemType == nwItemType.NO_TYPE
|
|
||||||
theItem.setType("NO_TYPE")
|
|
||||||
assert theItem.itemType == nwItemType.NO_TYPE
|
|
||||||
theItem.setType("ROOT")
|
|
||||||
assert theItem.itemType == nwItemType.ROOT
|
|
||||||
theItem.setType("FOLDER")
|
|
||||||
assert theItem.itemType == nwItemType.FOLDER
|
|
||||||
theItem.setType("FILE")
|
|
||||||
assert theItem.itemType == nwItemType.FILE
|
|
||||||
theItem.setType("TRASH")
|
|
||||||
assert theItem.itemType == nwItemType.TRASH
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testItemLayoutSetter(nwDummy):
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theItem = NWItem(theProject)
|
theItem = NWItem(theProject)
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
@@ -190,14 +246,22 @@ def testItemLayoutSetter(nwDummy):
|
|||||||
assert theItem.itemLayout == nwItemLayout.SCENE
|
assert theItem.itemLayout == nwItemLayout.SCENE
|
||||||
theItem.setLayout("NOTE")
|
theItem.setLayout("NOTE")
|
||||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||||
|
theItem.setLayout(nwItemLayout.NOTE)
|
||||||
|
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||||
|
|
||||||
@pytest.mark.project
|
# END Test testCoreItem_LayoutSetter
|
||||||
def testItemXMLPackUnpack(nwDummy):
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
@pytest.mark.core
|
||||||
theItem = NWItem(theProject)
|
def testCoreItem_XMLPackUnpack(dummyGUI):
|
||||||
|
"""Test packing and unpacking XML objects for the NWItem class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
nwXML = etree.Element("novelWriterXML")
|
nwXML = etree.Element("novelWriterXML")
|
||||||
|
|
||||||
|
# File
|
||||||
|
# ====
|
||||||
|
|
||||||
|
theItem = NWItem(theProject)
|
||||||
theItem.setHandle("0123456789abc")
|
theItem.setHandle("0123456789abc")
|
||||||
theItem.setParent("0123456789abc")
|
theItem.setParent("0123456789abc")
|
||||||
theItem.setOrder(1)
|
theItem.setOrder(1)
|
||||||
@@ -206,7 +270,7 @@ def testItemXMLPackUnpack(nwDummy):
|
|||||||
theItem.setType("FILE")
|
theItem.setType("FILE")
|
||||||
theItem.setStatus("Main")
|
theItem.setStatus("Main")
|
||||||
theItem.setLayout("NOTE")
|
theItem.setLayout("NOTE")
|
||||||
theItem.setExpanded(True)
|
theItem.setExported(False)
|
||||||
theItem.setParaCount(3)
|
theItem.setParaCount(3)
|
||||||
theItem.setWordCount(5)
|
theItem.setWordCount(5)
|
||||||
theItem.setCharCount(7)
|
theItem.setCharCount(7)
|
||||||
@@ -219,17 +283,18 @@ def testItemXMLPackUnpack(nwDummy):
|
|||||||
b"<content>"
|
b"<content>"
|
||||||
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||||
b"<name>A Name</name><type>FILE</type><class>NOVEL</class><status>New</status>"
|
b"<name>A Name</name><type>FILE</type><class>NOVEL</class><status>New</status>"
|
||||||
b"<exported>True</exported><layout>NOTE</layout><charCount>7</charCount>"
|
b"<exported>False</exported><layout>NOTE</layout><charCount>7</charCount>"
|
||||||
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
|
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
|
||||||
b"</content>"
|
b"</content>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Unpack
|
# Unpack
|
||||||
|
theItem = NWItem(theProject)
|
||||||
assert theItem.unpackXML(xContent[0])
|
assert theItem.unpackXML(xContent[0])
|
||||||
assert theItem.itemHandle == "0123456789abc"
|
assert theItem.itemHandle == "0123456789abc"
|
||||||
assert theItem.itemParent == "0123456789abc"
|
assert theItem.itemParent == "0123456789abc"
|
||||||
assert theItem.itemOrder == 1
|
assert theItem.itemOrder == 1
|
||||||
assert theItem.isExpanded
|
assert theItem.isExported is False
|
||||||
assert theItem.paraCount == 3
|
assert theItem.paraCount == 3
|
||||||
assert theItem.wordCount == 5
|
assert theItem.wordCount == 5
|
||||||
assert theItem.charCount == 7
|
assert theItem.charCount == 7
|
||||||
@@ -238,6 +303,52 @@ def testItemXMLPackUnpack(nwDummy):
|
|||||||
assert theItem.itemType == nwItemType.FILE
|
assert theItem.itemType == nwItemType.FILE
|
||||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||||
|
|
||||||
|
# Folder
|
||||||
|
# ======
|
||||||
|
|
||||||
|
theItem = NWItem(theProject)
|
||||||
|
theItem.setHandle("0123456789abc")
|
||||||
|
theItem.setParent("0123456789abc")
|
||||||
|
theItem.setOrder(1)
|
||||||
|
theItem.setName("A Name")
|
||||||
|
theItem.setClass("NOVEL")
|
||||||
|
theItem.setType("FOLDER")
|
||||||
|
theItem.setStatus("Main")
|
||||||
|
theItem.setLayout("NOTE")
|
||||||
|
theItem.setExpanded(True)
|
||||||
|
theItem.setExported(False)
|
||||||
|
theItem.setParaCount(3)
|
||||||
|
theItem.setWordCount(5)
|
||||||
|
theItem.setCharCount(7)
|
||||||
|
theItem.setCursorPos(11)
|
||||||
|
|
||||||
|
# Pack
|
||||||
|
xContent = etree.SubElement(nwXML, "content")
|
||||||
|
theItem.packXML(xContent)
|
||||||
|
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
|
||||||
|
b"<content>"
|
||||||
|
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||||
|
b"<name>A Name</name><type>FOLDER</type><class>NOVEL</class><status>New</status>"
|
||||||
|
b"<expanded>True</expanded></item>"
|
||||||
|
b"</content>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unpack
|
||||||
|
theItem = NWItem(theProject)
|
||||||
|
assert theItem.unpackXML(xContent[0])
|
||||||
|
assert theItem.itemHandle == "0123456789abc"
|
||||||
|
assert theItem.itemParent == "0123456789abc"
|
||||||
|
assert theItem.itemOrder == 1
|
||||||
|
assert theItem.isExpanded is True
|
||||||
|
assert theItem.isExported is True
|
||||||
|
assert theItem.paraCount == 0
|
||||||
|
assert theItem.wordCount == 0
|
||||||
|
assert theItem.charCount == 0
|
||||||
|
assert theItem.cursorPos == 0
|
||||||
|
assert theItem.itemClass == nwItemClass.NOVEL
|
||||||
|
assert theItem.itemType == nwItemType.FOLDER
|
||||||
|
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||||
|
|
||||||
# Errors
|
# Errors
|
||||||
|
|
||||||
## Not an Item
|
## Not an Item
|
||||||
@@ -268,3 +379,5 @@ def testItemXMLPackUnpack(nwDummy):
|
|||||||
assert etree.tostring(xDummy, pretty_print=False, encoding="utf-8") == (
|
assert etree.tostring(xDummy, pretty_print=False, encoding="utf-8") == (
|
||||||
b"<group/>"
|
b"<group/>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# END Test testCoreItem_XMLPackUnpack
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter OptionState Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dummy import causeOSError
|
||||||
|
|
||||||
|
from nw.core import NWProject
|
||||||
|
from nw.core.options import OptionState
|
||||||
|
from nw.constants import nwFiles
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreOptions_LoadSave(monkeypatch, dummyGUI, tmpDir):
|
||||||
|
"""Test loading and saving from the OptionState class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theOpts = OptionState(theProject)
|
||||||
|
|
||||||
|
# Write a test file
|
||||||
|
optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE)
|
||||||
|
with open(optFile, mode="w+", encoding="utf8") as outFile:
|
||||||
|
json.dump({
|
||||||
|
"GuiBuildNovel": {
|
||||||
|
"winWidth": 1000,
|
||||||
|
"winHeight": 700,
|
||||||
|
"addNovel": True,
|
||||||
|
"addNotes": False,
|
||||||
|
"textFont": "Cantarell",
|
||||||
|
"dummyItem": None,
|
||||||
|
},
|
||||||
|
"DummyGroup": {
|
||||||
|
"dummyItem": None,
|
||||||
|
},
|
||||||
|
}, outFile)
|
||||||
|
|
||||||
|
# Load and save with no path set
|
||||||
|
theProject.projMeta = None
|
||||||
|
assert not theOpts.loadSettings()
|
||||||
|
assert not theOpts.saveSettings()
|
||||||
|
|
||||||
|
# Set path
|
||||||
|
theProject.projMeta = tmpDir
|
||||||
|
assert theProject.projMeta == tmpDir
|
||||||
|
|
||||||
|
# Cause open() to fail
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
assert not theOpts.loadSettings()
|
||||||
|
assert not theOpts.saveSettings()
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load proper
|
||||||
|
assert theOpts.loadSettings()
|
||||||
|
|
||||||
|
# Check that unwanted items have been removed
|
||||||
|
assert theOpts.theState == {
|
||||||
|
"GuiBuildNovel": {
|
||||||
|
"winWidth": 1000,
|
||||||
|
"winHeight": 700,
|
||||||
|
"addNovel": True,
|
||||||
|
"addNotes": False,
|
||||||
|
"textFont": "Cantarell",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save proper
|
||||||
|
assert theOpts.saveSettings()
|
||||||
|
|
||||||
|
# Load again to check we get the values back
|
||||||
|
assert theOpts.loadSettings()
|
||||||
|
assert theOpts.theState == {
|
||||||
|
"GuiBuildNovel": {
|
||||||
|
"winWidth": 1000,
|
||||||
|
"winHeight": 700,
|
||||||
|
"addNovel": True,
|
||||||
|
"addNotes": False,
|
||||||
|
"textFont": "Cantarell",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# END Test testCoreOptions_LoadSave
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreOptions_SetGet(monkeypatch, dummyGUI, tmpDir):
|
||||||
|
"""Test setting and getting values from the OptionState class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theOpts = OptionState(theProject)
|
||||||
|
|
||||||
|
# Set invalid values
|
||||||
|
assert not theOpts.setValue("DummyGroup", "dummyItem", None)
|
||||||
|
assert not theOpts.setValue("GuiBuildNovel", "dummyItem", None)
|
||||||
|
|
||||||
|
# Set valid value
|
||||||
|
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
|
||||||
|
|
||||||
|
# Set some values of different types
|
||||||
|
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
|
||||||
|
assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34)
|
||||||
|
assert theOpts.setValue("GuiBuildNovel", "addNovel", True)
|
||||||
|
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
|
||||||
|
|
||||||
|
# Generic get, doesn't check type
|
||||||
|
assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100
|
||||||
|
assert theOpts.getValue("GuiBuildNovel", "winHeight", None) == 12.34
|
||||||
|
assert theOpts.getValue("GuiBuildNovel", "addNovel", None) is True
|
||||||
|
assert theOpts.getValue("GuiBuildNovel", "textFont", None) == "Cantarell"
|
||||||
|
assert theOpts.getValue("GuiBuildNovel", "dummyItem", None) is None
|
||||||
|
|
||||||
|
# Get type-specific
|
||||||
|
assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100"
|
||||||
|
assert theOpts.getString("GuiBuildNovel", "dummyItem", None) is None
|
||||||
|
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
|
||||||
|
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None
|
||||||
|
assert theOpts.getInt("GuiBuildNovel", "dummyItem", None) is None
|
||||||
|
assert theOpts.getFloat("GuiBuildNovel", "winWidth", None) == 100.0
|
||||||
|
assert theOpts.getFloat("GuiBuildNovel", "textFont", None) is None
|
||||||
|
assert theOpts.getFloat("GuiBuildNovel", "dummyItem", None) is None
|
||||||
|
assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True
|
||||||
|
assert theOpts.getBool("GuiBuildNovel", "dummyItem", None) is None
|
||||||
|
|
||||||
|
# Check integer validators
|
||||||
|
assert theOpts.validIntRange(5, 0, 9, 3) == 5
|
||||||
|
assert theOpts.validIntRange(5, 0, 4, 3) == 3
|
||||||
|
assert theOpts.validIntRange(5, 0, 5, 3) == 5
|
||||||
|
assert theOpts.validIntRange(0, 0, 5, 3) == 0
|
||||||
|
|
||||||
|
assert theOpts.validIntTuple(0, (0, 1, 2), 3) == 0
|
||||||
|
assert theOpts.validIntTuple(5, (0, 1, 2), 3) == 3
|
||||||
|
|
||||||
|
# END Test testCoreOptions_SetGet
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Spell Check Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dummy import causeOSError
|
||||||
|
from tools import readFile, writeFile
|
||||||
|
|
||||||
|
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
|
||||||
|
from nw.constants import nwConst
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreSpell_Super(monkeypatch, tmpDir, tmpConf):
|
||||||
|
"""Test the spell checker super class
|
||||||
|
"""
|
||||||
|
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||||
|
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||||
|
|
||||||
|
spChk = NWSpellCheck()
|
||||||
|
spChk.mainConf = tmpConf
|
||||||
|
|
||||||
|
# Check that dummy functions return results that reflects that spell
|
||||||
|
# checking is effectively disabled
|
||||||
|
assert spChk.setLanguage("", "") is None
|
||||||
|
assert spChk.checkWord("")
|
||||||
|
assert spChk.suggestWords("") == []
|
||||||
|
assert spChk.listDictionaries() == []
|
||||||
|
assert spChk.describeDict() == ("", "")
|
||||||
|
|
||||||
|
# Check language info
|
||||||
|
assert NWSpellCheck.expandLanguage("en") == "English"
|
||||||
|
assert NWSpellCheck.expandLanguage("en_GB") == "English (GB)"
|
||||||
|
|
||||||
|
# Add a word to the user's dictionary
|
||||||
|
assert spChk._readProjectDictionary("dummy") is False
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
assert spChk._readProjectDictionary(wList) is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
assert spChk._readProjectDictionary(wList) is True
|
||||||
|
assert spChk.projectDict == wList
|
||||||
|
|
||||||
|
# Cannot write to file
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
assert spChk.addWord("d_word") is False
|
||||||
|
monkeypatch.undo()
|
||||||
|
assert readFile(wList) == "a_word\nb_word\nc_word\n"
|
||||||
|
|
||||||
|
# First time, OK
|
||||||
|
assert spChk.addWord("d_word") is True
|
||||||
|
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
|
||||||
|
|
||||||
|
# But not added twice
|
||||||
|
assert spChk.addWord("d_word") is False
|
||||||
|
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
|
||||||
|
|
||||||
|
# END Test testCoreSpell_Super
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreSpell_Enchant(monkeypatch, tmpDir, tmpConf):
|
||||||
|
"""Test the pyenchant spell checker
|
||||||
|
"""
|
||||||
|
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||||
|
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||||
|
|
||||||
|
# Block the enchant package (and trigger the dummy class)
|
||||||
|
monkeypatch.setitem(sys.modules, "enchant", None)
|
||||||
|
spChk = NWSpellEnchant()
|
||||||
|
|
||||||
|
spChk.setLanguage("en", wList)
|
||||||
|
assert spChk.setLanguage("", "") is None
|
||||||
|
assert spChk.checkWord("")
|
||||||
|
assert spChk.suggestWords("") == []
|
||||||
|
assert spChk.listDictionaries() == []
|
||||||
|
assert spChk.describeDict() == ("", "")
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load the proper enchant package
|
||||||
|
spChk = NWSpellEnchant()
|
||||||
|
spChk.mainConf = tmpConf
|
||||||
|
spChk.setLanguage("en", wList)
|
||||||
|
|
||||||
|
assert spChk.checkWord("a_word")
|
||||||
|
assert spChk.checkWord("b_word")
|
||||||
|
assert spChk.checkWord("c_word")
|
||||||
|
assert not spChk.checkWord("d_word")
|
||||||
|
|
||||||
|
spChk.addWord("d_word")
|
||||||
|
assert spChk.checkWord("d_word")
|
||||||
|
|
||||||
|
wSuggest = spChk.suggestWords("wrod")
|
||||||
|
assert len(wSuggest) > 0
|
||||||
|
assert "word" in wSuggest
|
||||||
|
|
||||||
|
dList = spChk.listDictionaries()
|
||||||
|
assert len(dList) > 0
|
||||||
|
|
||||||
|
aTag, aName = spChk.describeDict()
|
||||||
|
assert aTag == "en"
|
||||||
|
assert aName != ""
|
||||||
|
|
||||||
|
# END Test testCoreSpell_Enchant
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf):
|
||||||
|
"""Test the fallback simple spell checker
|
||||||
|
"""
|
||||||
|
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||||
|
wDict = os.path.join(tmpDir, "en.dict")
|
||||||
|
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||||
|
writeFile(wDict, "# Comment\ne_word\nf_word\ng_word\n")
|
||||||
|
|
||||||
|
spChk = NWSpellSimple()
|
||||||
|
spChk.mainConf = tmpConf
|
||||||
|
spChk.mainConf.dictPath = tmpDir
|
||||||
|
|
||||||
|
# Load dictionary, but fail
|
||||||
|
monkeypatch.setattr("builtins.open", causeOSError)
|
||||||
|
spChk.setLanguage("en", wList)
|
||||||
|
assert spChk.spellLanguage is None
|
||||||
|
assert spChk.WORDS == spChk.projDict
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Load dictionary properly
|
||||||
|
spChk.setLanguage("en", wList)
|
||||||
|
assert spChk.projDict == ["a_word", "b_word", "c_word"]
|
||||||
|
assert spChk.WORDS == ["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"]
|
||||||
|
|
||||||
|
# Check words
|
||||||
|
assert spChk.checkWord("a_word")
|
||||||
|
assert spChk.checkWord("b_word")
|
||||||
|
assert spChk.checkWord("c_word")
|
||||||
|
assert not spChk.checkWord("d_word")
|
||||||
|
assert spChk.checkWord("e_word")
|
||||||
|
assert spChk.checkWord("f_word")
|
||||||
|
assert spChk.checkWord("g_word")
|
||||||
|
|
||||||
|
# Add word
|
||||||
|
spChk.addWord("d_word")
|
||||||
|
assert spChk.checkWord("d_word")
|
||||||
|
|
||||||
|
# Check spelling
|
||||||
|
assert spChk.suggestWords(" \t") == []
|
||||||
|
|
||||||
|
wSuggest = spChk.suggestWords("d_wrod")
|
||||||
|
assert len(wSuggest) > 0
|
||||||
|
assert "d_word" in wSuggest
|
||||||
|
|
||||||
|
# Break the matching
|
||||||
|
monkeypatch.setattr("difflib.get_close_matches", lambda *args, **kwargs: [""])
|
||||||
|
assert spChk.suggestWords("word") == []
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# Capitalisation
|
||||||
|
wSuggest = spChk.suggestWords("D_wrod")
|
||||||
|
assert len(wSuggest) > 0
|
||||||
|
assert "D_word" in wSuggest
|
||||||
|
|
||||||
|
# List dictionaries
|
||||||
|
assert spChk.listDictionaries() == [("en", "English [%s]" % nwConst.SP_INTERNAL)]
|
||||||
|
|
||||||
|
# Description
|
||||||
|
aTag, aName = spChk.describeDict()
|
||||||
|
assert aTag == "en"
|
||||||
|
assert aName == nwConst.SP_INTERNAL
|
||||||
|
|
||||||
|
# END Test testCoreSpell_Simple
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Status Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from nw.core.status import NWStatus
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreStatus_Entries():
|
||||||
|
"""Test all the simple setters for the NWItem class.
|
||||||
|
"""
|
||||||
|
theStatus = NWStatus()
|
||||||
|
|
||||||
|
# Add entries
|
||||||
|
theStatus.addEntry("New", (100, 100, 100))
|
||||||
|
theStatus.addEntry("Minor", (200, 50, 0))
|
||||||
|
theStatus.addEntry("Major", (200, 150, 0))
|
||||||
|
theStatus.addEntry("Main", (50, 200, 0))
|
||||||
|
|
||||||
|
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||||
|
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||||
|
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||||
|
assert theStatus._theMap["New"] == 0
|
||||||
|
assert theStatus._theMap["Minor"] == 1
|
||||||
|
assert theStatus._theMap["Major"] == 2
|
||||||
|
assert theStatus._theMap["Main"] == 3
|
||||||
|
assert theStatus._theLength == 4
|
||||||
|
|
||||||
|
# Lookups
|
||||||
|
assert theStatus.lookupEntry(None) is None
|
||||||
|
assert theStatus.lookupEntry("dummy") is None
|
||||||
|
assert theStatus.lookupEntry("Main") == 3
|
||||||
|
|
||||||
|
# Checks
|
||||||
|
assert theStatus.checkEntry(123) == "New"
|
||||||
|
assert theStatus.checkEntry("Stuff") == "New"
|
||||||
|
assert theStatus.checkEntry("New ") == "New"
|
||||||
|
assert theStatus.checkEntry(" Main ") == "Main"
|
||||||
|
|
||||||
|
# Set new list
|
||||||
|
newList = [
|
||||||
|
("New", 1, 1, 1, "New"),
|
||||||
|
("Minor", 2, 2, 2, "Minor"),
|
||||||
|
("Major", 3, 3, 3, "Major"),
|
||||||
|
("Min", 4, 4, 4, "Main"),
|
||||||
|
("Max", 5, 5, 5, None),
|
||||||
|
]
|
||||||
|
assert theStatus.setNewEntries(None) == {}
|
||||||
|
assert theStatus.setNewEntries(newList) == {"Main": "Min"}
|
||||||
|
|
||||||
|
assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"]
|
||||||
|
assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)]
|
||||||
|
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||||
|
assert theStatus._theMap["New"] == 0
|
||||||
|
assert theStatus._theMap["Minor"] == 1
|
||||||
|
assert theStatus._theMap["Major"] == 2
|
||||||
|
assert theStatus._theMap["Min"] == 3
|
||||||
|
assert theStatus._theMap["Max"] == 4
|
||||||
|
assert theStatus._theLength == 5
|
||||||
|
|
||||||
|
# Add counts
|
||||||
|
countTo = [3, 5, 7, 9, 11]
|
||||||
|
for i, n in enumerate(countTo):
|
||||||
|
for _ in range(n):
|
||||||
|
theStatus.countEntry(theStatus._theLabels[i])
|
||||||
|
assert theStatus._theCounts == countTo
|
||||||
|
|
||||||
|
# Iterate
|
||||||
|
for i, (sA, sB, sC) in enumerate(theStatus):
|
||||||
|
assert sA == theStatus._theLabels[i]
|
||||||
|
assert sB == theStatus._theColours[i]
|
||||||
|
assert sC == theStatus._theCounts[i]
|
||||||
|
|
||||||
|
assert theStatus[9] == (None, None, None)
|
||||||
|
|
||||||
|
# Clear counts
|
||||||
|
theStatus.resetCounts()
|
||||||
|
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||||
|
|
||||||
|
# END Test testCoreStatus_Entries
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreStatus_XMLPackUnpack():
|
||||||
|
"""Test all the simple setters for the NWItem class.
|
||||||
|
"""
|
||||||
|
theStatus = NWStatus()
|
||||||
|
theStatus.addEntry("New", (100, 100, 100))
|
||||||
|
theStatus.addEntry("Minor", (200, 50, 0))
|
||||||
|
theStatus.addEntry("Major", (200, 150, 0))
|
||||||
|
theStatus.addEntry("Main", (50, 200, 0))
|
||||||
|
|
||||||
|
countTo = [3, 5, 7, 9]
|
||||||
|
for i, n in enumerate(countTo):
|
||||||
|
for _ in range(n):
|
||||||
|
theStatus.countEntry(theStatus._theLabels[i])
|
||||||
|
|
||||||
|
nwXML = etree.Element("novelWriterXML")
|
||||||
|
|
||||||
|
# Pack
|
||||||
|
xStatus = etree.SubElement(nwXML, "status")
|
||||||
|
theStatus.packXML(xStatus)
|
||||||
|
assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == (
|
||||||
|
b"<status>"
|
||||||
|
b"<entry blue=\"100\" green=\"100\" red=\"100\">New</entry>"
|
||||||
|
b"<entry blue=\"0\" green=\"50\" red=\"200\">Minor</entry>"
|
||||||
|
b"<entry blue=\"0\" green=\"150\" red=\"200\">Major</entry>"
|
||||||
|
b"<entry blue=\"0\" green=\"200\" red=\"50\">Main</entry>"
|
||||||
|
b"</status>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unpack
|
||||||
|
theStatus = NWStatus()
|
||||||
|
assert theStatus.unpackXML(xStatus)
|
||||||
|
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||||
|
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||||
|
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||||
|
assert theStatus._theMap["New"] == 0
|
||||||
|
assert theStatus._theMap["Minor"] == 1
|
||||||
|
assert theStatus._theMap["Major"] == 2
|
||||||
|
assert theStatus._theMap["Main"] == 3
|
||||||
|
assert theStatus._theLength == 4
|
||||||
|
|
||||||
|
# END Test testCoreStatus_XMLPackUnpack
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter ToHtml Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nw.core import NWProject, NWIndex, ToHtml
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToHtml_Format(dummyGUI):
|
||||||
|
"""Test all the formatters for the ToHtml class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
|
||||||
|
theHtml = ToHtml(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Export Mode
|
||||||
|
# ===========
|
||||||
|
|
||||||
|
assert theHtml._formatSynopsis("synopsis text") == (
|
||||||
|
"<p class='synopsis'><strong>Synopsis: </strong>synopsis text</p>\n"
|
||||||
|
)
|
||||||
|
assert theHtml._formatComments("comment text") == (
|
||||||
|
"<p class='comment'><strong>Comment: </strong>comment text</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert theHtml._formatKeywords("") == ""
|
||||||
|
assert theHtml._formatKeywords("tag: Jane") == (
|
||||||
|
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
|
||||||
|
)
|
||||||
|
assert theHtml._formatKeywords("char: Bod, Jane") == (
|
||||||
|
"<div>"
|
||||||
|
"<span class='tags'>Characters:</span> "
|
||||||
|
"<a href='#tag_Bod'>Bod</a>, "
|
||||||
|
"<a href='#tag_Jane'>Jane</a>"
|
||||||
|
"</div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Preview Mode
|
||||||
|
# ============
|
||||||
|
|
||||||
|
theHtml.setPreview(True, True)
|
||||||
|
|
||||||
|
assert theHtml._formatSynopsis("synopsis text") == (
|
||||||
|
"<p class='comment'><span class='synopsis'>Synopsis: </span>synopsis text</p>\n"
|
||||||
|
)
|
||||||
|
assert theHtml._formatComments("comment text") == (
|
||||||
|
"<p class='comment'>comment text</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert theHtml._formatKeywords("") == ""
|
||||||
|
assert theHtml._formatKeywords("tag: Jane") == (
|
||||||
|
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
|
||||||
|
)
|
||||||
|
assert theHtml._formatKeywords("char: Bod, Jane") == (
|
||||||
|
"<div>"
|
||||||
|
"<span class='tags'>Characters:</span> "
|
||||||
|
"<a href='#char=Bod'>Bod</a>, "
|
||||||
|
"<a href='#char=Jane'>Jane</a>"
|
||||||
|
"</div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# END Test testCoreToHtml_Format
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToHtml_Convert(dummyGUI):
|
||||||
|
"""Test the converter of the ToHtml class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
|
||||||
|
theHtml = ToHtml(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Export Mode
|
||||||
|
# ===========
|
||||||
|
|
||||||
|
theHtml.isNovel = True
|
||||||
|
|
||||||
|
# Header 1
|
||||||
|
theHtml.theText = "# Title\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h1 class='title'>Title</h1>\n"
|
||||||
|
|
||||||
|
# Header 2
|
||||||
|
theHtml.theText = "## Chapter Title\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h1>Chapter Title</h1>\n"
|
||||||
|
|
||||||
|
# Header 3
|
||||||
|
theHtml.theText = "### Scene Title\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h2>Scene Title</h2>\n"
|
||||||
|
|
||||||
|
# Header 4
|
||||||
|
theHtml.theText = "#### Section Title\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h3>Section Title</h3>\n"
|
||||||
|
|
||||||
|
theHtml.isNovel = False
|
||||||
|
theHtml.setLinkHeaders(True)
|
||||||
|
|
||||||
|
# Header 1
|
||||||
|
theHtml.theText = "# Heading One\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h1><a name='T000001'></a>Heading One</h1>\n"
|
||||||
|
|
||||||
|
# Header 2
|
||||||
|
theHtml.theText = "## Heading Two\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h2><a name='T000001'></a>Heading Two</h2>\n"
|
||||||
|
|
||||||
|
# Header 3
|
||||||
|
theHtml.theText = "### Heading Three\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h3><a name='T000001'></a>Heading Three</h3>\n"
|
||||||
|
|
||||||
|
# Header 4
|
||||||
|
theHtml.theText = "#### Heading Four\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<h4><a name='T000001'></a>Heading Four</h4>\n"
|
||||||
|
|
||||||
|
# Text
|
||||||
|
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p>Some <strong>nested bold and <em>italic</em> and "
|
||||||
|
"<del>strikethrough</del> text</strong> here</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Text w/Hard Break
|
||||||
|
theHtml.theText = "Line one \nLine two \nLine three\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p class='break'>Line one<br/>Line two<br/>Line three</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Synopsis
|
||||||
|
theHtml.theText = "%synopsis: The synopsis ...\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == ""
|
||||||
|
|
||||||
|
theHtml.setSynopsis(True)
|
||||||
|
theHtml.theText = "%synopsis: The synopsis ...\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p class='synopsis'><strong>Synopsis: </strong>The synopsis ...</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Comment
|
||||||
|
theHtml.theText = "% A comment ...\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == ""
|
||||||
|
|
||||||
|
theHtml.setComments(True)
|
||||||
|
theHtml.theText = "% A comment ...\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p class='comment'><strong>Comment: </strong>A comment ...</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keywords
|
||||||
|
theHtml.theText = "@char: Bod, Jane\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == ""
|
||||||
|
|
||||||
|
theHtml.setKeywords(True)
|
||||||
|
theHtml.theText = "@char: Bod, Jane\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<div><span class='tags'>Characters:</span> "
|
||||||
|
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Direct Tests
|
||||||
|
# ============
|
||||||
|
|
||||||
|
theHtml.isNovel = True
|
||||||
|
|
||||||
|
# Title
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE),
|
||||||
|
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' style='text-align: center; page-break-before: never;'>"
|
||||||
|
"<a name='T000001'></a>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_SEP, 1, "* * *", None, theHtml.A_CENTRE),
|
||||||
|
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<p class='sep'>* * *</p>\n"
|
||||||
|
|
||||||
|
# Skip
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_SKIP, 1, "", None, theHtml.A_NONE),
|
||||||
|
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == "<p class='skip'> </p>\n"
|
||||||
|
|
||||||
|
# Styles
|
||||||
|
# ======
|
||||||
|
|
||||||
|
theHtml.setLinkHeaders(False)
|
||||||
|
|
||||||
|
# Align Left
|
||||||
|
theHtml.setStyles(False)
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
theHtml.setStyles(True)
|
||||||
|
|
||||||
|
# Align Left
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' style='text-align: left;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Align Right
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_RIGHT),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' style='text-align: right;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Align Centre
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_CENTRE),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' style='text-align: center;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Align Justify
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_JUSTIFY),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' style='text-align: justify;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Page Break Always
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB | theHtml.A_PBA),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' "
|
||||||
|
"style='page-break-before: always; page-break-after: always;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Page Break Avoid
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' "
|
||||||
|
"style='page-break-before: avoid; page-break-after: avoid;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Page Break ANever
|
||||||
|
theHtml.theTokens = [
|
||||||
|
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO),
|
||||||
|
]
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<h1 class='title' "
|
||||||
|
"style='page-break-before: never; page-break-after: never;'>A Title</h1>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Preview Mode
|
||||||
|
# ============
|
||||||
|
|
||||||
|
theHtml.setPreview(True, True)
|
||||||
|
|
||||||
|
# Text (HTML4)
|
||||||
|
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p>Some <b>nested bold and <i>italic</i> and "
|
||||||
|
"<span style='text-decoration: line-through;'>strikethrough</span> "
|
||||||
|
"text</b> here</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# END Test testCoreToHtml_Convert
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToHtml_Methods(dummyGUI):
|
||||||
|
"""Test all the other methods of the ToHtml class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theHtml = ToHtml(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Auto-Replace
|
||||||
|
docText = "Text with <brackets> & short–dash, long—dash …\n"
|
||||||
|
theHtml.theText = docText
|
||||||
|
theHtml.doAutoReplace()
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theResult == (
|
||||||
|
"<p>Text with <brackets> & short–dash, long—dash …</p>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revert on MD
|
||||||
|
assert theHtml.theMarkdown == (
|
||||||
|
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||||
|
)
|
||||||
|
theHtml.doPostProcessing()
|
||||||
|
assert theHtml.theMarkdown == docText + "\n"
|
||||||
|
|
||||||
|
# With Preview, No Revert
|
||||||
|
theHtml.setPreview(True, True)
|
||||||
|
theHtml.theText = docText
|
||||||
|
theHtml.doAutoReplace()
|
||||||
|
theHtml.tokenizeText()
|
||||||
|
theHtml.doConvert()
|
||||||
|
assert theHtml.theMarkdown == (
|
||||||
|
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||||
|
)
|
||||||
|
theHtml.doPostProcessing()
|
||||||
|
assert theHtml.theMarkdown == (
|
||||||
|
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# CSS
|
||||||
|
# ===
|
||||||
|
|
||||||
|
assert len(theHtml.getStyleSheet()) > 1
|
||||||
|
assert "p {text-align: left;}" in theHtml.getStyleSheet()
|
||||||
|
assert "p {text-align: justify;}" not in theHtml.getStyleSheet()
|
||||||
|
|
||||||
|
theHtml.setJustify(True)
|
||||||
|
assert "p {text-align: left;}" not in theHtml.getStyleSheet()
|
||||||
|
assert "p {text-align: justify;}" in theHtml.getStyleSheet()
|
||||||
|
|
||||||
|
theHtml.setStyles(False)
|
||||||
|
assert theHtml.getStyleSheet() == []
|
||||||
|
|
||||||
|
# END Test testCoreToHtml_Methods
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Tokenizer Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nw.core import NWProject, NWDoc
|
||||||
|
from nw.core.tokenizer import Tokenizer
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToken_Setters(dummyGUI):
|
||||||
|
"""Test all the setters for the Tokenizer class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theToken = Tokenizer(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Verify defaults
|
||||||
|
assert theToken.fmtTitle == "%title%"
|
||||||
|
assert theToken.fmtChapter == "%title%"
|
||||||
|
assert theToken.fmtUnNum == "%title%"
|
||||||
|
assert theToken.fmtScene == "%title%"
|
||||||
|
assert theToken.fmtSection == "%title%"
|
||||||
|
assert theToken.hideScene is False
|
||||||
|
assert theToken.hideSection is False
|
||||||
|
assert theToken.linkHeaders is False
|
||||||
|
assert theToken.doBodyText is True
|
||||||
|
assert theToken.doSynopsis is False
|
||||||
|
assert theToken.doComments is False
|
||||||
|
assert theToken.doKeywords is False
|
||||||
|
assert theToken.doJustify is False
|
||||||
|
|
||||||
|
# Set new values
|
||||||
|
theToken.setTitleFormat("T: %title%")
|
||||||
|
theToken.setChapterFormat("C: %title%")
|
||||||
|
theToken.setUnNumberedFormat("U: %title%")
|
||||||
|
theToken.setSceneFormat("S: %title%", True)
|
||||||
|
theToken.setSectionFormat("X: %title%", True)
|
||||||
|
theToken.setLinkHeaders(True)
|
||||||
|
theToken.setBodyText(False)
|
||||||
|
theToken.setSynopsis(True)
|
||||||
|
theToken.setComments(True)
|
||||||
|
theToken.setKeywords(True)
|
||||||
|
theToken.setJustify(True)
|
||||||
|
|
||||||
|
# Check new values
|
||||||
|
assert theToken.fmtTitle == "T: %title%"
|
||||||
|
assert theToken.fmtChapter == "C: %title%"
|
||||||
|
assert theToken.fmtUnNum == "U: %title%"
|
||||||
|
assert theToken.fmtScene == "S: %title%"
|
||||||
|
assert theToken.fmtSection == "X: %title%"
|
||||||
|
assert theToken.hideScene is True
|
||||||
|
assert theToken.hideSection is True
|
||||||
|
assert theToken.linkHeaders is True
|
||||||
|
assert theToken.doBodyText is False
|
||||||
|
assert theToken.doSynopsis is True
|
||||||
|
assert theToken.doComments is True
|
||||||
|
assert theToken.doKeywords is True
|
||||||
|
assert theToken.doJustify is True
|
||||||
|
|
||||||
|
# END Test testCoreToken_Setters
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
|
||||||
|
"""Test handling files and text in the Tokenizer class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theProject.projTree.setSeed(42)
|
||||||
|
theToken = Tokenizer(theProject, dummyGUI)
|
||||||
|
|
||||||
|
assert theProject.openProject(nwMinimal)
|
||||||
|
sHandle = "8c659a11cd429"
|
||||||
|
|
||||||
|
# Set some content to work with
|
||||||
|
|
||||||
|
docText = (
|
||||||
|
"### Scene Six\n\n"
|
||||||
|
"This is text with _italic text_, some **bold text**, some ~~deleted text~~, "
|
||||||
|
"and some **_mixed text_** and **some _nested_ text**.\n\n"
|
||||||
|
"#### Replace\n\n"
|
||||||
|
"Also, replace <A> and <B>.\n\n"
|
||||||
|
)
|
||||||
|
docTextR = docText.replace("<A>", "this").replace("<B>", "that")
|
||||||
|
|
||||||
|
nDoc = NWDoc(theProject, dummyGUI)
|
||||||
|
nDoc.openDocument(sHandle)
|
||||||
|
nDoc.saveDocument(docText)
|
||||||
|
nDoc.clearDocument()
|
||||||
|
|
||||||
|
theProject.setAutoReplace({"A": "this", "B": "that"})
|
||||||
|
|
||||||
|
assert theProject.saveProject()
|
||||||
|
|
||||||
|
# Root heading
|
||||||
|
assert theToken.addRootHeading("dummy") is False
|
||||||
|
assert theToken.addRootHeading(sHandle) is False
|
||||||
|
assert theToken.addRootHeading("7695ce551d265") is True
|
||||||
|
assert theToken.theMarkdown == "# Notes: Plot\n\n"
|
||||||
|
|
||||||
|
# Set text
|
||||||
|
assert theToken.setText("dummy") is False
|
||||||
|
assert theToken.setText(sHandle) is True
|
||||||
|
assert theToken.theText == docText
|
||||||
|
|
||||||
|
monkeypatch.setattr("nw.constants.nwConst.MAX_DOCSIZE", 100)
|
||||||
|
assert theToken.setText(sHandle, docText) is True
|
||||||
|
assert theToken.theText == (
|
||||||
|
"# ERROR\n\n"
|
||||||
|
"Document 'New Scene' is too big (0.00 MB). Skipping.\n\n"
|
||||||
|
)
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
assert theToken.setText(sHandle, docText) is True
|
||||||
|
assert theToken.theText == docText
|
||||||
|
|
||||||
|
assert theToken.isNone is False
|
||||||
|
assert theToken.isTitle is False
|
||||||
|
assert theToken.isBook is False
|
||||||
|
assert theToken.isPage is False
|
||||||
|
assert theToken.isPart is False
|
||||||
|
assert theToken.isUnNum is False
|
||||||
|
assert theToken.isChap is False
|
||||||
|
assert theToken.isScene is True
|
||||||
|
assert theToken.isNote is False
|
||||||
|
assert theToken.isNovel is True
|
||||||
|
|
||||||
|
# Auto replace
|
||||||
|
theToken.doAutoReplace()
|
||||||
|
assert theToken.theText == docTextR
|
||||||
|
|
||||||
|
# Access
|
||||||
|
assert theToken.getResult() is None
|
||||||
|
assert theToken.getResultSize() == 0
|
||||||
|
theToken.theResult = ""
|
||||||
|
assert theToken.getResultSize() == 0
|
||||||
|
|
||||||
|
# Post Processing
|
||||||
|
theToken.theResult = r"This is text with escapes: \** \~~ \__"
|
||||||
|
theToken.doPostProcessing()
|
||||||
|
assert theToken.theResult == "This is text with escapes: ** ~~ __"
|
||||||
|
|
||||||
|
# END Test testCoreToken_TextOps
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToken_Tokenize(dummyGUI):
|
||||||
|
"""Test the tokenization of the Tokenizer class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theToken = Tokenizer(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Header 1
|
||||||
|
theToken.theText = "# Novel Title\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "# Novel Title\n\n"
|
||||||
|
|
||||||
|
# Header 2
|
||||||
|
theToken.theText = "## Chapter One\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "## Chapter One\n\n"
|
||||||
|
|
||||||
|
# Header 3
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "### Scene One\n\n"
|
||||||
|
|
||||||
|
# Header 4
|
||||||
|
theToken.theText = "#### A Section\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "#### A Section\n\n"
|
||||||
|
|
||||||
|
# Comment
|
||||||
|
theToken.theText = "% A comment\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "\n"
|
||||||
|
|
||||||
|
theToken.setComments(True)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theMarkdown == "% A comment\n\n"
|
||||||
|
|
||||||
|
# Symopsis
|
||||||
|
theToken.theText = "%synopsis: The synopsis\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
theToken.theText = "% synopsis: The synopsis\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "\n"
|
||||||
|
|
||||||
|
theToken.setSynopsis(True)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theMarkdown == "% synopsis: The synopsis\n\n"
|
||||||
|
|
||||||
|
# Keyword
|
||||||
|
theToken.theText = "@char: Bod\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "\n"
|
||||||
|
|
||||||
|
theToken.setKeywords(True)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theMarkdown == "@char: Bod\n\n"
|
||||||
|
|
||||||
|
# Text
|
||||||
|
theToken.theText = "Some plain text\non two lines\n\n\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_TEXT, 1, "Some plain text", [], Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_TEXT, 2, "on two lines", [], Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n"
|
||||||
|
|
||||||
|
theToken.setBodyText(False)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "\n\n\n"
|
||||||
|
theToken.setBodyText(True)
|
||||||
|
|
||||||
|
# Text Emphasis
|
||||||
|
theToken.theText = "Some **bolded text** on this lines\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(
|
||||||
|
Tokenizer.T_TEXT, 1,
|
||||||
|
"Some **bolded text** on this lines",
|
||||||
|
[
|
||||||
|
[5, 2, Tokenizer.FMT_B_B],
|
||||||
|
[18, 2, Tokenizer.FMT_B_E],
|
||||||
|
],
|
||||||
|
Tokenizer.A_NONE
|
||||||
|
),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n"
|
||||||
|
|
||||||
|
theToken.theText = "Some _italic text_ on this lines\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(
|
||||||
|
Tokenizer.T_TEXT, 1,
|
||||||
|
"Some _italic text_ on this lines",
|
||||||
|
[
|
||||||
|
[5, 1, Tokenizer.FMT_I_B],
|
||||||
|
[17, 1, Tokenizer.FMT_I_E],
|
||||||
|
],
|
||||||
|
Tokenizer.A_NONE
|
||||||
|
),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n"
|
||||||
|
|
||||||
|
theToken.theText = "Some **_bold italic text_** on this lines\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(
|
||||||
|
Tokenizer.T_TEXT, 1,
|
||||||
|
"Some **_bold italic text_** on this lines",
|
||||||
|
[
|
||||||
|
[5, 2, Tokenizer.FMT_B_B],
|
||||||
|
[7, 1, Tokenizer.FMT_I_B],
|
||||||
|
[24, 1, Tokenizer.FMT_I_E],
|
||||||
|
[25, 2, Tokenizer.FMT_B_E],
|
||||||
|
],
|
||||||
|
Tokenizer.A_NONE
|
||||||
|
),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n"
|
||||||
|
|
||||||
|
theToken.theText = "Some ~~strikethrough text~~ on this lines\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(
|
||||||
|
Tokenizer.T_TEXT, 1,
|
||||||
|
"Some ~~strikethrough text~~ on this lines",
|
||||||
|
[
|
||||||
|
[5, 2, Tokenizer.FMT_D_B],
|
||||||
|
[25, 2, Tokenizer.FMT_D_E],
|
||||||
|
],
|
||||||
|
Tokenizer.A_NONE
|
||||||
|
),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n"
|
||||||
|
|
||||||
|
theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(
|
||||||
|
Tokenizer.T_TEXT, 1,
|
||||||
|
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here",
|
||||||
|
[
|
||||||
|
[5, 2, Tokenizer.FMT_B_B],
|
||||||
|
[23, 1, Tokenizer.FMT_I_B],
|
||||||
|
[30, 1, Tokenizer.FMT_I_E],
|
||||||
|
[36, 2, Tokenizer.FMT_D_B],
|
||||||
|
[51, 2, Tokenizer.FMT_D_E],
|
||||||
|
[58, 2, Tokenizer.FMT_B_E],
|
||||||
|
],
|
||||||
|
Tokenizer.A_NONE
|
||||||
|
),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
assert theToken.theMarkdown == (
|
||||||
|
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the markdown function as well
|
||||||
|
assert theToken.getFilteredMarkdown() == (
|
||||||
|
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# END Test testCoreToken_Tokenize
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreToken_Headers(dummyGUI):
|
||||||
|
"""Test the header and page parser of the Tokenizer class.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theToken = Tokenizer(theProject, dummyGUI)
|
||||||
|
|
||||||
|
# Nothing
|
||||||
|
theToken.theText = "Some text ...\n"
|
||||||
|
assert theToken.doHeaders() is True
|
||||||
|
theToken.isNone = True
|
||||||
|
assert theToken.doHeaders() is False
|
||||||
|
theToken.isNone = False
|
||||||
|
assert theToken.doHeaders() is True
|
||||||
|
theToken.isNote = True
|
||||||
|
assert theToken.doHeaders() is False
|
||||||
|
theToken.isNote = False
|
||||||
|
|
||||||
|
##
|
||||||
|
# Novel
|
||||||
|
##
|
||||||
|
|
||||||
|
theToken.isNovel = True
|
||||||
|
|
||||||
|
# Titles
|
||||||
|
# ======
|
||||||
|
|
||||||
|
# H1: Title
|
||||||
|
theToken.theText = "# Novel Title\n"
|
||||||
|
theToken.setTitleFormat(r"T: %title%")
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD1, 1, "T: Novel Title", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Chapters
|
||||||
|
# ========
|
||||||
|
|
||||||
|
# H2: Chapter
|
||||||
|
theToken.theText = "## Chapter One\n"
|
||||||
|
theToken.setChapterFormat(r"C: %title%")
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "C: Chapter One", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H2: Unnumbered Chapter
|
||||||
|
theToken.theText = "## Chapter One\n"
|
||||||
|
theToken.setUnNumberedFormat(r"U: %title%")
|
||||||
|
theToken.isUnNum = True
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "U: Chapter One", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H2: Unnumbered Chapter with Star
|
||||||
|
theToken.theText = "## *Prologue\n"
|
||||||
|
theToken.setUnNumberedFormat(r"U: %title%")
|
||||||
|
theToken.isUnNum = False
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "U: Prologue", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H2: Chapter Word Number
|
||||||
|
theToken.theText = "## Chapter\n"
|
||||||
|
theToken.setChapterFormat(r"Chapter %chw%")
|
||||||
|
theToken.numChapter = 0
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H2: Chapter Roman Number Upper Case
|
||||||
|
theToken.theText = "## Chapter\n"
|
||||||
|
theToken.setChapterFormat(r"Chapter %chI%")
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "Chapter II", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H2: Chapter Roman Number Lower Case
|
||||||
|
theToken.theText = "## Chapter\n"
|
||||||
|
theToken.setChapterFormat(r"Chapter %chi%")
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD2, 1, "Chapter iii", None, Tokenizer.A_PBB),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Scenes
|
||||||
|
# ======
|
||||||
|
|
||||||
|
# H3: Scene w/Title
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"S: %title%", False)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD3, 1, "S: Scene One", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene Hidden wo/Format
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"", True)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene wo/Format, first
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"", False)
|
||||||
|
theToken.firstScene = True
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene wo/Format, not first
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"", False)
|
||||||
|
theToken.firstScene = False
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene Separator, first
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"* * *", False)
|
||||||
|
theToken.firstScene = True
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene Separator, not first
|
||||||
|
theToken.theText = "### Scene One\n"
|
||||||
|
theToken.setSceneFormat(r"* * *", False)
|
||||||
|
theToken.firstScene = False
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene w/Absolute Number
|
||||||
|
theToken.theText = "### A Scene\n"
|
||||||
|
theToken.setSceneFormat(r"Scene %sca%", False)
|
||||||
|
theToken.numAbsScene = 0
|
||||||
|
theToken.numChScene = 0
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD3, 1, "Scene 1", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H3: Scene w/Chapter Number
|
||||||
|
theToken.theText = "### A Scene\n"
|
||||||
|
theToken.setSceneFormat(r"Scene %ch%.%sc%", False)
|
||||||
|
theToken.numAbsScene = 0
|
||||||
|
theToken.numChScene = 1
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD3, 1, "Scene 3.2", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sections
|
||||||
|
# ========
|
||||||
|
|
||||||
|
# H4: Section Hidden wo/Format
|
||||||
|
theToken.theText = "#### A Section\n"
|
||||||
|
theToken.setSectionFormat(r"", True)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H4: Section Visible wo/Format
|
||||||
|
theToken.theText = "#### A Section\n"
|
||||||
|
theToken.setSectionFormat(r"", False)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H4: Section w/Format
|
||||||
|
theToken.theText = "#### A Section\n"
|
||||||
|
theToken.setSectionFormat(r"X: %title%", False)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD4, 1, "X: A Section", None, Tokenizer.A_NONE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H4: Section Separator
|
||||||
|
theToken.theText = "#### A Section\n"
|
||||||
|
theToken.setSectionFormat(r"* * *", False)
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check the first scene detector
|
||||||
|
assert theToken.firstScene is False
|
||||||
|
theToken.firstScene = True
|
||||||
|
assert theToken.firstScene is True
|
||||||
|
theToken.theText = "Some text ...\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.firstScene is False
|
||||||
|
|
||||||
|
##
|
||||||
|
# Title or Partition
|
||||||
|
##
|
||||||
|
|
||||||
|
theToken.isNovel = False
|
||||||
|
|
||||||
|
# H1: Title
|
||||||
|
theToken.theText = "# Novel Title\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.isTitle = True
|
||||||
|
theToken.isPart = False
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
|
||||||
|
]
|
||||||
|
|
||||||
|
# H1: Partition
|
||||||
|
theToken.theText = "# Partition Title\n"
|
||||||
|
theToken.setTitleFormat(r"T: %title%")
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.isTitle = False
|
||||||
|
theToken.isPart = True
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_HEAD1, 1, "Partition Title", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE),
|
||||||
|
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
|
||||||
|
]
|
||||||
|
|
||||||
|
##
|
||||||
|
# Page
|
||||||
|
##
|
||||||
|
|
||||||
|
theToken.isNovel = False
|
||||||
|
theToken.isTitle = False
|
||||||
|
theToken.isPart = False
|
||||||
|
theToken.isPage = True
|
||||||
|
|
||||||
|
# Some Page Text
|
||||||
|
theToken.theText = "Page text\n\nMore text\n"
|
||||||
|
theToken.tokenizeText()
|
||||||
|
theToken.doHeaders()
|
||||||
|
assert theToken.theTokens == [
|
||||||
|
(Tokenizer.T_TEXT, 1, "Page text", [], Tokenizer.A_PBB | Tokenizer.A_LEFT),
|
||||||
|
(Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_LEFT),
|
||||||
|
(Tokenizer.T_TEXT, 3, "More text", [], Tokenizer.A_LEFT),
|
||||||
|
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_LEFT),
|
||||||
|
]
|
||||||
|
|
||||||
|
# END Test testCoreToken_Headers
|
||||||
@@ -7,7 +7,7 @@ import pytest
|
|||||||
from nw.core.tools import countWords, numberToRoman, numberToWord
|
from nw.core.tools import countWords, numberToRoman, numberToWord
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCountWords():
|
def testCoreTools_CountWords():
|
||||||
"""Test the word counter and the exclusion filers.
|
"""Test the word counter and the exclusion filers.
|
||||||
"""
|
"""
|
||||||
testText = (
|
testText = (
|
||||||
@@ -26,15 +26,46 @@ def testCountWords():
|
|||||||
"\n"
|
"\n"
|
||||||
"\n"
|
"\n"
|
||||||
"The third paragraph.\n"
|
"The third paragraph.\n"
|
||||||
|
"\n"
|
||||||
|
"Dashes\u2013and even longer\u2014dashes."
|
||||||
)
|
)
|
||||||
cC, wC, pC = countWords(testText)
|
cC, wC, pC = countWords(testText)
|
||||||
|
|
||||||
assert cC == 108
|
assert cC == 138
|
||||||
assert wC == 17
|
assert wC == 22
|
||||||
assert pC == 3
|
assert pC == 4
|
||||||
|
|
||||||
|
# END Test testCoreTools_CountWords
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testNumberWords():
|
def testCoreTools_RomanNumbers():
|
||||||
|
"""Test conversion of integers to Roman numbers.
|
||||||
|
"""
|
||||||
|
assert numberToRoman(None, False) == "NAN"
|
||||||
|
assert numberToRoman(0, False) == "OOR"
|
||||||
|
assert numberToRoman(1, False) == "I"
|
||||||
|
assert numberToRoman(2, False) == "II"
|
||||||
|
assert numberToRoman(3, False) == "III"
|
||||||
|
assert numberToRoman(4, False) == "IV"
|
||||||
|
assert numberToRoman(5, False) == "V"
|
||||||
|
assert numberToRoman(6, False) == "VI"
|
||||||
|
assert numberToRoman(7, False) == "VII"
|
||||||
|
assert numberToRoman(8, False) == "VIII"
|
||||||
|
assert numberToRoman(9, False) == "IX"
|
||||||
|
assert numberToRoman(10, False) == "X"
|
||||||
|
assert numberToRoman(14, False) == "XIV"
|
||||||
|
assert numberToRoman(42, False) == "XLII"
|
||||||
|
assert numberToRoman(99, False) == "XCIX"
|
||||||
|
assert numberToRoman(142, False) == "CXLII"
|
||||||
|
assert numberToRoman(542, False) == "DXLII"
|
||||||
|
assert numberToRoman(999, False) == "CMXCIX"
|
||||||
|
assert numberToRoman(2010, False) == "MMX"
|
||||||
|
assert numberToRoman(999, True) == "cmxcix"
|
||||||
|
|
||||||
|
# END Test testCoreTools_RomanNumbers
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTools_NumberWords():
|
||||||
"""Test the conversion of integer to English words.
|
"""Test the conversion of integer to English words.
|
||||||
"""
|
"""
|
||||||
assert numberToWord(0, "en") == "Zero"
|
assert numberToWord(0, "en") == "Zero"
|
||||||
@@ -70,27 +101,9 @@ def testNumberWords():
|
|||||||
assert numberToWord(2, "foo") == "Two"
|
assert numberToWord(2, "foo") == "Two"
|
||||||
assert numberToWord(3, "foo") == "Three"
|
assert numberToWord(3, "foo") == "Three"
|
||||||
|
|
||||||
@pytest.mark.core
|
# Test out of range values
|
||||||
def testRomanNumbers():
|
assert numberToWord(12345, "en") == "[Out of Range]"
|
||||||
"""Test conversion of integers to Roman numbers.
|
assert numberToWord(-2345, "en") == "[Negative]"
|
||||||
"""
|
assert numberToWord("234", "en") == "[NaN]"
|
||||||
assert numberToRoman(None, False) == "NAN"
|
|
||||||
assert numberToRoman(0, False) == "OOR"
|
# END Test testCoreTools_NumberWords
|
||||||
assert numberToRoman(1, False) == "I"
|
|
||||||
assert numberToRoman(2, False) == "II"
|
|
||||||
assert numberToRoman(3, False) == "III"
|
|
||||||
assert numberToRoman(4, False) == "IV"
|
|
||||||
assert numberToRoman(5, False) == "V"
|
|
||||||
assert numberToRoman(6, False) == "VI"
|
|
||||||
assert numberToRoman(7, False) == "VII"
|
|
||||||
assert numberToRoman(8, False) == "VIII"
|
|
||||||
assert numberToRoman(9, False) == "IX"
|
|
||||||
assert numberToRoman(10, False) == "X"
|
|
||||||
assert numberToRoman(14, False) == "XIV"
|
|
||||||
assert numberToRoman(42, False) == "XLII"
|
|
||||||
assert numberToRoman(99, False) == "XCIX"
|
|
||||||
assert numberToRoman(142, False) == "CXLII"
|
|
||||||
assert numberToRoman(542, False) == "DXLII"
|
|
||||||
assert numberToRoman(999, False) == "CMXCIX"
|
|
||||||
assert numberToRoman(2010, False) == "MMX"
|
|
||||||
assert numberToRoman(999, True) == "cmxcix"
|
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter NWTree Class Tester
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from nw.core.project import NWProject, NWItem, NWTree
|
||||||
|
from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def dummyItems(dummyGUI):
|
||||||
|
"""Create a list of dummy items.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
|
||||||
|
itemA = NWItem(theProject)
|
||||||
|
itemA.itemName = "Novel"
|
||||||
|
itemA.itemType = nwItemType.ROOT
|
||||||
|
itemA.itemClass = nwItemClass.NOVEL
|
||||||
|
itemA.isExpanded = True
|
||||||
|
|
||||||
|
itemB = NWItem(theProject)
|
||||||
|
itemB.itemName = "Act One"
|
||||||
|
itemB.itemType = nwItemType.FOLDER
|
||||||
|
itemB.itemClass = nwItemClass.NOVEL
|
||||||
|
itemB.isExpanded = True
|
||||||
|
|
||||||
|
itemC = NWItem(theProject)
|
||||||
|
itemC.itemName = "Chapter One"
|
||||||
|
itemC.itemType = nwItemType.FILE
|
||||||
|
itemC.itemClass = nwItemClass.NOVEL
|
||||||
|
itemC.itemLayout = nwItemLayout.CHAPTER
|
||||||
|
itemC.charCount = 300
|
||||||
|
itemC.wordCount = 50
|
||||||
|
itemC.paraCount = 2
|
||||||
|
|
||||||
|
itemD = NWItem(theProject)
|
||||||
|
itemD.itemName = "Scene One"
|
||||||
|
itemD.itemType = nwItemType.FILE
|
||||||
|
itemD.itemClass = nwItemClass.NOVEL
|
||||||
|
itemD.itemLayout = nwItemLayout.SCENE
|
||||||
|
itemD.charCount = 3000
|
||||||
|
itemD.wordCount = 500
|
||||||
|
itemD.paraCount = 20
|
||||||
|
|
||||||
|
itemE = NWItem(theProject)
|
||||||
|
itemE.itemName = "Outtakes"
|
||||||
|
itemE.itemType = nwItemType.ROOT
|
||||||
|
itemE.itemClass = nwItemClass.ARCHIVE
|
||||||
|
itemE.isExpanded = False
|
||||||
|
|
||||||
|
itemF = NWItem(theProject)
|
||||||
|
itemF.itemName = "Trash"
|
||||||
|
itemF.itemType = nwItemType.TRASH
|
||||||
|
itemF.itemClass = nwItemClass.TRASH
|
||||||
|
itemF.isExpanded = False
|
||||||
|
|
||||||
|
itemG = NWItem(theProject)
|
||||||
|
itemG.itemName = "Characters"
|
||||||
|
itemG.itemType = nwItemType.ROOT
|
||||||
|
itemG.itemClass = nwItemClass.CHARACTER
|
||||||
|
itemG.isExpanded = True
|
||||||
|
|
||||||
|
itemH = NWItem(theProject)
|
||||||
|
itemH.itemName = "Jane Doe"
|
||||||
|
itemH.itemType = nwItemType.FILE
|
||||||
|
itemH.itemClass = nwItemClass.CHARACTER
|
||||||
|
itemH.itemLayout = nwItemLayout.NOTE
|
||||||
|
itemH.charCount = 2000
|
||||||
|
itemH.wordCount = 400
|
||||||
|
itemH.paraCount = 16
|
||||||
|
|
||||||
|
theItems = [
|
||||||
|
("a000000000001", None, itemA),
|
||||||
|
("b000000000001", "a000000000001", itemB),
|
||||||
|
("c000000000001", "b000000000001", itemC),
|
||||||
|
("c000000000002", "b000000000001", itemD),
|
||||||
|
("a000000000002", None, itemE),
|
||||||
|
("a000000000003", None, itemF),
|
||||||
|
("a000000000004", None, itemG),
|
||||||
|
("b000000000002", "a000000000002", itemH),
|
||||||
|
]
|
||||||
|
|
||||||
|
return theItems
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_BuildTree(dummyGUI, dummyItems):
|
||||||
|
"""Test building a project tree from a list of items.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
theTree.setSeed(42)
|
||||||
|
assert theTree._handleSeed == 42
|
||||||
|
|
||||||
|
# Check that tree is empty (calls NWTree.__bool__)
|
||||||
|
assert not theTree
|
||||||
|
|
||||||
|
# Check for archive and trash folders
|
||||||
|
assert theTree.trashRoot() is None
|
||||||
|
assert theTree.archiveRoot() is None
|
||||||
|
assert not theTree.isTrashRoot("a000000000003")
|
||||||
|
|
||||||
|
aHandles = []
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
aHandles.append(tHandle)
|
||||||
|
assert theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert theTree._treeChanged
|
||||||
|
|
||||||
|
# Check that tree is not empty (calls __bool__)
|
||||||
|
assert theTree
|
||||||
|
|
||||||
|
# Check the number of elements (calls __len__)
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
# Check that we have the correct handles
|
||||||
|
assert theTree.handles() == aHandles
|
||||||
|
|
||||||
|
# Check by iterator (calls __iter__, __next__ and __getitem__)
|
||||||
|
for theItem, theHandle in zip(theTree, aHandles):
|
||||||
|
assert theItem.itemHandle == theHandle
|
||||||
|
|
||||||
|
# Check that we have the correct archive and trash folders
|
||||||
|
assert theTree.trashRoot() == "a000000000003"
|
||||||
|
assert theTree.archiveRoot() == "a000000000002"
|
||||||
|
assert theTree.isTrashRoot("a000000000003")
|
||||||
|
|
||||||
|
# Try to add another trash folder
|
||||||
|
itemT = NWItem(theProject)
|
||||||
|
itemT.itemName = "Trash"
|
||||||
|
itemT.itemType = nwItemType.TRASH
|
||||||
|
itemT.itemClass = nwItemClass.TRASH
|
||||||
|
itemT.isExpanded = False
|
||||||
|
|
||||||
|
assert not theTree.append("1234567890abc", None, itemT)
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
# Generate handle automatically
|
||||||
|
itemT = NWItem(theProject)
|
||||||
|
itemT.itemName = "New File"
|
||||||
|
itemT.itemType = nwItemType.FILE
|
||||||
|
itemT.itemClass = nwItemClass.NOVEL
|
||||||
|
itemT.itemLayout = nwItemLayout.SCENE
|
||||||
|
|
||||||
|
assert theTree.append(None, None, itemT)
|
||||||
|
assert len(theTree) == len(dummyItems) + 1
|
||||||
|
|
||||||
|
theList = theTree.handles()
|
||||||
|
assert theList[-1] == "73475cb40a568"
|
||||||
|
|
||||||
|
# Try to add existing handle
|
||||||
|
assert not theTree.append("73475cb40a568", None, itemT)
|
||||||
|
assert len(theTree) == len(dummyItems) + 1
|
||||||
|
|
||||||
|
# Delete a non-existing item
|
||||||
|
del theTree["dummy"]
|
||||||
|
assert len(theTree) == len(dummyItems) + 1
|
||||||
|
|
||||||
|
# Delete the last item
|
||||||
|
del theTree["73475cb40a568"]
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
assert "73475cb40a568" not in theTree
|
||||||
|
|
||||||
|
# Delete the Novel, Archive and Trash folders
|
||||||
|
del theTree["a000000000001"]
|
||||||
|
assert len(theTree) == len(dummyItems) - 1
|
||||||
|
assert "a000000000001" not in theTree
|
||||||
|
|
||||||
|
del theTree["a000000000002"]
|
||||||
|
assert len(theTree) == len(dummyItems) - 2
|
||||||
|
assert "a000000000002" not in theTree
|
||||||
|
assert theTree.archiveRoot() is None
|
||||||
|
|
||||||
|
del theTree["a000000000003"]
|
||||||
|
assert len(theTree) == len(dummyItems) - 3
|
||||||
|
assert "a000000000003" not in theTree
|
||||||
|
assert theTree.trashRoot() is None
|
||||||
|
|
||||||
|
# END Test testCoreTree_BuildTree
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_Methods(dummyGUI, dummyItems):
|
||||||
|
"""Test building a project tree from a list of items.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
# Root item lookup
|
||||||
|
theTree._treeRoots.append("dummy")
|
||||||
|
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||||
|
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
|
||||||
|
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
|
||||||
|
|
||||||
|
# Check for root uniqueness
|
||||||
|
assert theTree.checkRootUnique(nwItemClass.CUSTOM)
|
||||||
|
assert theTree.checkRootUnique(nwItemClass.WORLD)
|
||||||
|
assert not theTree.checkRootUnique(nwItemClass.NOVEL)
|
||||||
|
assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
|
||||||
|
|
||||||
|
# Find root item of child item
|
||||||
|
assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
|
||||||
|
assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
|
||||||
|
assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
|
||||||
|
assert theTree.getRootItem("dummy") is None
|
||||||
|
|
||||||
|
# Get item path
|
||||||
|
assert theTree.getItemPath("dummy") == []
|
||||||
|
assert theTree.getItemPath("c000000000001") == [
|
||||||
|
"c000000000001", "b000000000001", "a000000000001"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Break the folder parent handle
|
||||||
|
theTree["b000000000001"].itemParent = "dummy"
|
||||||
|
assert theTree.getItemPath("c000000000001") == [
|
||||||
|
"c000000000001", "b000000000001"
|
||||||
|
]
|
||||||
|
|
||||||
|
theTree["b000000000001"].itemParent = "a000000000001"
|
||||||
|
assert theTree.getItemPath("c000000000001") == [
|
||||||
|
"c000000000001", "b000000000001", "a000000000001"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Change file layout
|
||||||
|
assert not theTree.setFileItemLayout("dummy", nwItemLayout.UNNUMBERED)
|
||||||
|
assert not theTree.setFileItemLayout("b000000000001", nwItemLayout.UNNUMBERED)
|
||||||
|
assert not theTree.setFileItemLayout("c000000000001", "stuff")
|
||||||
|
assert theTree.setFileItemLayout("c000000000001", nwItemLayout.UNNUMBERED)
|
||||||
|
assert theTree["c000000000001"].itemLayout == nwItemLayout.UNNUMBERED
|
||||||
|
|
||||||
|
# END Test testCoreTree_Methods
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_MakeHandles(monkeypatch, dummyGUI):
|
||||||
|
"""Test generating item handles.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
theTree.setSeed(42)
|
||||||
|
|
||||||
|
tHandle = theTree._makeHandle()
|
||||||
|
assert tHandle == "73475cb40a568"
|
||||||
|
|
||||||
|
# Add the next in line to the project to foprce duplicate
|
||||||
|
theTree._projTree["44cb730c42048"] = None
|
||||||
|
tHandle = theTree._makeHandle()
|
||||||
|
assert tHandle == "71ee45a3c0db9"
|
||||||
|
|
||||||
|
# Fix the time() function and force a handle collission
|
||||||
|
theTree.setSeed(None)
|
||||||
|
monkeypatch.setattr("nw.core.tree.time", lambda: 123.4)
|
||||||
|
|
||||||
|
tHandle = theTree._makeHandle()
|
||||||
|
theTree._projTree[tHandle] = None
|
||||||
|
assert tHandle == "5f466d7afa48b"
|
||||||
|
|
||||||
|
tHandle = theTree._makeHandle()
|
||||||
|
theTree._projTree[tHandle] = None
|
||||||
|
assert tHandle == "a79acf4c634a7"
|
||||||
|
|
||||||
|
monkeypatch.undo()
|
||||||
|
|
||||||
|
# END Test testCoreTree_MakeHandles
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_Stats(dummyGUI, dummyItems):
|
||||||
|
"""Test project stats methods.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
theTree._treeOrder.append("dummy")
|
||||||
|
|
||||||
|
# Count Words
|
||||||
|
novelWords, noteWords = theTree.sumWords()
|
||||||
|
assert novelWords == 550
|
||||||
|
assert noteWords == 400
|
||||||
|
|
||||||
|
# Count types
|
||||||
|
nRoot, nFolder, nFile = theTree.countTypes()
|
||||||
|
assert nRoot == 3
|
||||||
|
assert nFolder == 1
|
||||||
|
assert nFile == 3
|
||||||
|
|
||||||
|
# END Test testCoreTree_Stats
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_Reorder(dummyGUI, dummyItems):
|
||||||
|
"""Test changing tree order.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
aHandle = []
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
aHandle.append(tHandle)
|
||||||
|
theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
bHandle = aHandle.copy()
|
||||||
|
bHandle[2], bHandle[3] = bHandle[3], bHandle[2]
|
||||||
|
assert aHandle != bHandle
|
||||||
|
|
||||||
|
assert theTree.handles() == aHandle
|
||||||
|
theTree.setOrder(bHandle)
|
||||||
|
assert theTree.handles() == bHandle
|
||||||
|
|
||||||
|
theTree.setOrder(bHandle + ["dummy"])
|
||||||
|
assert theTree.handles() == bHandle
|
||||||
|
|
||||||
|
theTree._treeOrder.append("dummy")
|
||||||
|
theTree.setOrder(bHandle)
|
||||||
|
assert theTree.handles() == bHandle
|
||||||
|
|
||||||
|
# END Test testCoreTree_Reorder
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems):
|
||||||
|
"""Test changing tree order.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
nwXML = etree.Element("novelWriterXML")
|
||||||
|
theTree.packXML(nwXML)
|
||||||
|
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
|
||||||
|
b"<novelWriterXML>"
|
||||||
|
b"<content count=\"8\">"
|
||||||
|
b"<item handle=\"a000000000001\" order=\"0\" parent=\"None\">"
|
||||||
|
b"<name>Novel</name><type>ROOT</type><class>NOVEL</class><status>None</status>"
|
||||||
|
b"<expanded>True</expanded></item>"
|
||||||
|
b"<item handle=\"b000000000001\" order=\"0\" parent=\"a000000000001\">"
|
||||||
|
b"<name>Act One</name><type>FOLDER</type><class>NOVEL</class><status>None</status>"
|
||||||
|
b"<expanded>True</expanded></item>"
|
||||||
|
b"<item handle=\"c000000000001\" order=\"0\" parent=\"b000000000001\">"
|
||||||
|
b"<name>Chapter One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||||
|
b"<exported>True</exported><layout>CHAPTER</layout><charCount>300</charCount>"
|
||||||
|
b"<wordCount>50</wordCount><paraCount>2</paraCount><cursorPos>0</cursorPos></item>"
|
||||||
|
b"<item handle=\"c000000000002\" order=\"0\" parent=\"b000000000001\">"
|
||||||
|
b"<name>Scene One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||||
|
b"<exported>True</exported><layout>SCENE</layout><charCount>3000</charCount>"
|
||||||
|
b"<wordCount>500</wordCount><paraCount>20</paraCount><cursorPos>0</cursorPos></item>"
|
||||||
|
b"<item handle=\"a000000000002\" order=\"0\" parent=\"None\">"
|
||||||
|
b"<name>Outtakes</name><type>ROOT</type><class>ARCHIVE</class><status>None</status>"
|
||||||
|
b"<expanded>False</expanded></item>"
|
||||||
|
b"<item handle=\"a000000000003\" order=\"0\" parent=\"None\">"
|
||||||
|
b"<name>Trash</name><type>TRASH</type><class>TRASH</class><status>None</status>"
|
||||||
|
b"<expanded>False</expanded></item>"
|
||||||
|
b"<item handle=\"a000000000004\" order=\"0\" parent=\"None\">"
|
||||||
|
b"<name>Characters</name><type>ROOT</type><class>CHARACTER</class><status>None</status>"
|
||||||
|
b"<expanded>True</expanded></item>"
|
||||||
|
b"<item handle=\"b000000000002\" order=\"0\" parent=\"a000000000002\">"
|
||||||
|
b"<name>Jane Doe</name><type>FILE</type><class>CHARACTER</class><status>None</status>"
|
||||||
|
b"<exported>True</exported><layout>NOTE</layout><charCount>2000</charCount>"
|
||||||
|
b"<wordCount>400</wordCount><paraCount>16</paraCount><cursorPos>0</cursorPos></item>"
|
||||||
|
b"</content></novelWriterXML>"
|
||||||
|
)
|
||||||
|
|
||||||
|
theTree.clear()
|
||||||
|
assert len(theTree) == 0
|
||||||
|
assert not theTree.unpackXML(nwXML)
|
||||||
|
assert theTree.unpackXML(nwXML[0])
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
|
||||||
|
# END Test testCoreTree_XMLPackUnpack
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir):
|
||||||
|
"""Test writing the ToC.txt file.
|
||||||
|
"""
|
||||||
|
theProject = NWProject(dummyGUI)
|
||||||
|
theTree = NWTree(theProject)
|
||||||
|
|
||||||
|
for tHandle, pHande, nwItem in dummyItems:
|
||||||
|
theTree.append(tHandle, pHande, nwItem)
|
||||||
|
|
||||||
|
assert len(theTree) == len(dummyItems)
|
||||||
|
theTree._treeOrder.append("dummy")
|
||||||
|
|
||||||
|
def dummyIsFile(fileName):
|
||||||
|
"""Return True for items that are files in novelWriter and
|
||||||
|
should thus also be files in the project folder structure.
|
||||||
|
"""
|
||||||
|
dItem = theTree[fileName[8:21]]
|
||||||
|
assert dItem is not None
|
||||||
|
return dItem.itemType == nwItemType.FILE
|
||||||
|
|
||||||
|
monkeypatch.setattr("os.path.isfile", dummyIsFile)
|
||||||
|
|
||||||
|
theProject.projContent = "content"
|
||||||
|
theProject.projPath = None
|
||||||
|
assert not theTree.writeToCFile()
|
||||||
|
|
||||||
|
theProject.projPath = tmpDir
|
||||||
|
assert theTree.writeToCFile()
|
||||||
|
|
||||||
|
pathA = os.path.join("content", "c000000000001.nwd")
|
||||||
|
pathB = os.path.join("content", "c000000000002.nwd")
|
||||||
|
pathC = os.path.join("content", "b000000000002.nwd")
|
||||||
|
|
||||||
|
with open(os.path.join(tmpDir, nwFiles.TOC_TXT), mode="r", encoding="utf8") as inFile:
|
||||||
|
assert inFile.read() == (
|
||||||
|
"\n"
|
||||||
|
"Table of Contents\n"
|
||||||
|
"=================\n"
|
||||||
|
"\n"
|
||||||
|
"File Name Class Layout Document Label\n"
|
||||||
|
"-------------------------------------------------------------\n"
|
||||||
|
f"{pathA} NOVEL CHAPTER Chapter One\n"
|
||||||
|
f"{pathB} NOVEL SCENE Scene One\n"
|
||||||
|
f"{pathC} CHARACTER NOTE Jane Doe\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# END Test testCoreTree_ToCFile
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""novelWriter Error Tester
|
|
||||||
"""
|
|
||||||
|
|
||||||
import nw
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from PyQt5.QtWidgets import qApp
|
|
||||||
|
|
||||||
from nw.error import NWErrorMessage, exceptionHandler
|
|
||||||
|
|
||||||
@pytest.mark.error
|
|
||||||
def testErrorDialog(qtbot, nwFuncTemp, nwTemp):
|
|
||||||
qApp.closeAllWindows()
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
|
|
||||||
qtbot.addWidget(nwGUI)
|
|
||||||
nwGUI.show()
|
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
|
||||||
|
|
||||||
nwErr = NWErrorMessage(nwGUI)
|
|
||||||
qtbot.addWidget(nwErr)
|
|
||||||
nwErr.show()
|
|
||||||
|
|
||||||
# Invalid Error
|
|
||||||
nwErr.setMessage(Exception, "Faulty Error", 123)
|
|
||||||
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
|
|
||||||
|
|
||||||
# Valid Error
|
|
||||||
nwErr.setMessage(Exception, "First Error", None)
|
|
||||||
theMessage = nwErr.msgBody.toPlainText()
|
|
||||||
assert theMessage
|
|
||||||
assert "First Error" in theMessage
|
|
||||||
assert "Exception" in theMessage
|
|
||||||
nwErr._doClose()
|
|
||||||
nwErr.close()
|
|
||||||
|
|
||||||
theMessage = exceptionHandler(Exception, "Second Error", None, testMode=True)
|
|
||||||
assert theMessage
|
|
||||||
assert "Second Error" in theMessage
|
|
||||||
assert "Exception" in theMessage
|
|
||||||
|
|
||||||
nwGUI.closeMain()
|
|
||||||
|
|
||||||
# qtbot.stopForInteraction()
|
|
||||||
@@ -9,7 +9,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from nwtools import cmpFiles, getGuiItem
|
from tools import cmpFiles, getGuiItem
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QItemSelectionModel
|
from PyQt5.QtCore import Qt, QItemSelectionModel
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
@@ -30,8 +30,8 @@ typeDelay = 1
|
|||||||
stepDelay = 20
|
stepDelay = 20
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
def testProjectSettings(qtbot, monkeypatch, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -43,8 +43,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
|||||||
|
|
||||||
# Create new project
|
# Create new project
|
||||||
nwGUI.theProject.projTree.setSeed(42)
|
nwGUI.theProject.projTree.setSeed(42)
|
||||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
assert nwGUI.newProject({"projPath": fncDir})
|
||||||
nwGUI.mainConf.backupPath = nwFuncTemp
|
nwGUI.mainConf.backupPath = fncDir
|
||||||
|
|
||||||
# Get the dialog object
|
# Get the dialog object
|
||||||
monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None)
|
monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None)
|
||||||
@@ -135,9 +135,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
|||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
# Check the files
|
# Check the files
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||||
testFile = os.path.join(nwTempGUI, "2_nwProject.nwx")
|
testFile = os.path.join(nwTempGUI, "2_nwProject.nwx")
|
||||||
refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx")
|
refFile = os.path.join(refDir, "gui", "2_nwProject.nwx")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])
|
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
def testItemEditor(qtbot, yesToAll, monkeypatch, fncDir, nwTempGUI, refDir, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -154,7 +154,7 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
|||||||
|
|
||||||
# Create new, save, open project
|
# Create new, save, open project
|
||||||
nwGUI.theProject.projTree.setSeed(42)
|
nwGUI.theProject.projTree.setSeed(42)
|
||||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
assert nwGUI.newProject({"projPath": fncDir})
|
||||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||||
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
|
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
|
||||||
|
|
||||||
@@ -208,9 +208,9 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
|||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
# Check the files
|
# Check the files
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||||
testFile = os.path.join(nwTempGUI, "3_nwProject.nwx")
|
testFile = os.path.join(nwTempGUI, "3_nwProject.nwx")
|
||||||
refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx")
|
refFile = os.path.join(refDir, "gui", "3_nwProject.nwx")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||||
|
|
||||||
@@ -218,8 +218,8 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, fncDir, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -227,13 +227,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
|
|
||||||
# Create new, save, close project
|
# Create new, save, close project
|
||||||
nwGUI.theProject.projTree.setSeed(42)
|
nwGUI.theProject.projTree.setSeed(42)
|
||||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
assert nwGUI.newProject({"projPath": fncDir})
|
||||||
qtbot.wait(200)
|
qtbot.wait(200)
|
||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
assert nwGUI.closeProject()
|
assert nwGUI.closeProject()
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS)
|
sessFile = os.path.join(fncDir, "meta", nwFiles.SESS_STATS)
|
||||||
with open(sessFile, mode="w+", encoding="utf-8") as outFile:
|
with open(sessFile, mode="w+", encoding="utf-8") as outFile:
|
||||||
outFile.write(
|
outFile.write(
|
||||||
"# Start Time End Time Novel Notes\n"
|
"# Start Time End Time Novel Notes\n"
|
||||||
@@ -244,10 +244,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Open again, and check the stats
|
# Open again, and check the stats
|
||||||
assert nwGUI.openProject(nwFuncTemp)
|
assert nwGUI.openProject(fncDir)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
nwGUI.mainConf.lastPath = nwFuncTemp
|
nwGUI.mainConf.lastPath = fncDir
|
||||||
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
|
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
|
||||||
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
|
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.load(inFile)
|
jsonData = json.load(inFile)
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.loads(inFile.read())
|
jsonData = json.loads(inFile.read())
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.load(inFile)
|
jsonData = json.load(inFile)
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.load(inFile)
|
jsonData = json.load(inFile)
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.load(inFile)
|
jsonData = json.load(inFile)
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||||
jsonData = json.load(inFile)
|
jsonData = json.load(inFile)
|
||||||
|
|
||||||
@@ -357,8 +357,8 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
def testAboutBox(qtbot, monkeypatch, fncDir, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -374,12 +374,20 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
|||||||
msgAbout.show()
|
msgAbout.show()
|
||||||
|
|
||||||
assert msgAbout.pageAbout.document().characterCount() > 100
|
assert msgAbout.pageAbout.document().characterCount() > 100
|
||||||
|
assert msgAbout.pageNotes.document().characterCount() > 100
|
||||||
assert msgAbout.pageLicense.document().characterCount() > 100
|
assert msgAbout.pageLicense.document().characterCount() > 100
|
||||||
|
|
||||||
msgAbout.mainConf.guiLang = "whatever"
|
msgAbout.mainConf.assetPath = "whatever"
|
||||||
|
|
||||||
|
msgAbout._fillNotesPage()
|
||||||
|
assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..."
|
||||||
|
|
||||||
msgAbout._fillLicensePage()
|
msgAbout._fillLicensePage()
|
||||||
assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..."
|
assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..."
|
||||||
|
|
||||||
|
msgAbout.showReleaseNotes()
|
||||||
|
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
|
||||||
|
|
||||||
# Qt About
|
# Qt About
|
||||||
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None)
|
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None)
|
||||||
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
|
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
|
||||||
@@ -389,9 +397,9 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, refDir, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -420,13 +428,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
||||||
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd")
|
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd")
|
||||||
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.nwd")
|
refFile = os.path.join(refDir, "build", "1_LoremIpsum.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
||||||
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm")
|
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm")
|
||||||
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.htm")
|
refFile = os.path.join(refDir, "build", "1_LoremIpsum.htm")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -459,13 +467,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
||||||
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd")
|
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd")
|
||||||
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.nwd")
|
refFile = os.path.join(refDir, "build", "2_LoremIpsum.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
||||||
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm")
|
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm")
|
||||||
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.htm")
|
refFile = os.path.join(refDir, "build", "2_LoremIpsum.htm")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -479,14 +487,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
|
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
||||||
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd")
|
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd")
|
||||||
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.nwd")
|
refFile = os.path.join(refDir, "build", "3_LoremIpsum.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
|
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
||||||
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm")
|
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm")
|
||||||
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.htm")
|
refFile = os.path.join(refDir, "build", "3_LoremIpsum.htm")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -513,14 +521,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
|
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
||||||
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.nwd")
|
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.nwd")
|
||||||
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.nwd")
|
refFile = os.path.join(refDir, "build", "4_LoremIpsum.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
|
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
||||||
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.htm")
|
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.htm")
|
||||||
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.htm")
|
refFile = os.path.join(refDir, "build", "4_LoremIpsum.htm")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -528,14 +536,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
|
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
||||||
testFile = os.path.join(nwTempBuild, "4H_LoremIpsum.json")
|
testFile = os.path.join(nwTempBuild, "4H_LoremIpsum.json")
|
||||||
refFile = os.path.join(nwRef, "build", "4H_LoremIpsum.json")
|
refFile = os.path.join(refDir, "build", "4H_LoremIpsum.json")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [8])
|
assert cmpFiles(testFile, refFile, [8])
|
||||||
|
|
||||||
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
|
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
|
||||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
||||||
testFile = os.path.join(nwTempBuild, "4M_LoremIpsum.json")
|
testFile = os.path.join(nwTempBuild, "4M_LoremIpsum.json")
|
||||||
refFile = os.path.join(nwRef, "build", "4M_LoremIpsum.json")
|
refFile = os.path.join(refDir, "build", "4M_LoremIpsum.json")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [8])
|
assert cmpFiles(testFile, refFile, [8])
|
||||||
|
|
||||||
@@ -581,9 +589,9 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp):
|
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, refDir, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -612,7 +620,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
|||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd")
|
projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd")
|
testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
|
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -638,7 +646,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
|||||||
# This should give us back the file as it was before
|
# This should give us back the file as it was before
|
||||||
projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd")
|
projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd")
|
testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
|
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
||||||
|
|
||||||
@@ -662,19 +670,19 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
|||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd")
|
projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
|
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
|
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd")
|
projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd")
|
testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_31489056e0916.nwd")
|
refFile = os.path.join(refDir, "gui", "5_31489056e0916.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd")
|
projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd")
|
testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_98010bd9270f9.nwd")
|
refFile = os.path.join(refDir, "gui", "5_98010bd9270f9.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -700,31 +708,31 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
|||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd")
|
projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
|
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
|
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd")
|
projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd")
|
testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_031b4af5197ec.nwd")
|
refFile = os.path.join(refDir, "gui", "5_031b4af5197ec.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd")
|
projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd")
|
testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd")
|
refFile = os.path.join(refDir, "gui", "5_41cfc0d1f2d12.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd")
|
projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd")
|
testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_2858dcd1057d3.nwd")
|
refFile = os.path.join(refDir, "gui", "5_2858dcd1057d3.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd")
|
projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd")
|
testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "5_2fca346db6561.nwd")
|
refFile = os.path.join(refDir, "gui", "5_2fca346db6561.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -732,7 +740,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
|||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||||
|
|
||||||
if sys.platform.startswith("darwin"):
|
if sys.platform.startswith("darwin"):
|
||||||
# Disable for macOS because the test segfaults on QWizard.show()
|
# Disable for macOS because the test segfaults on QWizard.show()
|
||||||
@@ -744,7 +752,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
|||||||
ProjWizardCustomPage, ProjWizardFinalPage
|
ProjWizardCustomPage, ProjWizardFinalPage
|
||||||
)
|
)
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -929,8 +937,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1009,8 +1017,8 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf):
|
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir, refDir, tmpConf):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1195,20 +1203,21 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC
|
|||||||
# qtbot.stopForInteraction()
|
# qtbot.stopForInteraction()
|
||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
|
|
||||||
refConf = os.path.join(nwRef, "novelwriter_prefs.conf")
|
refConf = os.path.join(refDir, "novelwriter_prefs.conf")
|
||||||
projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
|
projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
|
||||||
testConf = os.path.join(nwTemp, "novelwriter_prefs.conf")
|
testConf = os.path.join(tmpDir, "novelwriter_prefs.conf")
|
||||||
copyfile(projConf, testConf)
|
copyfile(projConf, testConf)
|
||||||
ignoreLines = [
|
ignoreLines = [
|
||||||
2, # Timestamp
|
2, # Timestamp
|
||||||
11, 12, 13, 14, 15, 16, 17, # Window sizes
|
9, # Release Notes
|
||||||
7, 27, # Fonts (depends on system default)
|
12, 13, 14, 15, 16, 17, 18, # Window sizes
|
||||||
|
7, 28, # Fonts (depends on system default)
|
||||||
]
|
]
|
||||||
assert cmpFiles(testConf, refConf, ignoreLines)
|
assert cmpFiles(testConf, refConf, ignoreLines)
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
|
def testQuotesDialog(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1237,15 +1246,15 @@ def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp)
|
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir)
|
||||||
assert nwGUI.selectProjectPath() == nwTemp
|
assert nwGUI.selectProjectPath() == tmpDir
|
||||||
|
|
||||||
# qtbot.stopForInteraction()
|
# qtbot.stopForInteraction()
|
||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
@@ -4,12 +4,10 @@
|
|||||||
|
|
||||||
import nw
|
import nw
|
||||||
import pytest
|
import pytest
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from nwtools import cmpFiles
|
from tools import cmpFiles
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel
|
from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel
|
||||||
from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon, QTextBlock
|
from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon, QTextBlock
|
||||||
@@ -27,85 +25,9 @@ typeDelay = 1
|
|||||||
stepDelay = 20
|
stepDelay = 20
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
def testDocEditor(qtbot, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
|
||||||
|
|
||||||
# Defaults
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp, "--style=Fusion"]
|
|
||||||
)
|
|
||||||
assert nw.logger.getEffectiveLevel() == logging.WARNING
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
|
|
||||||
# Log Levels
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--info", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
assert nw.logger.getEffectiveLevel() == logging.INFO
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--debug", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
assert nw.logger.getEffectiveLevel() == logging.DEBUG
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--verbose", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
assert nw.logger.getEffectiveLevel() == 5
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
|
|
||||||
# Help and Version
|
|
||||||
with pytest.raises(SystemExit) as ex:
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
assert ex.value.code == 0
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit) as ex:
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--version", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
assert ex.value.code == 0
|
|
||||||
|
|
||||||
# Invalid options
|
|
||||||
with pytest.raises(SystemExit) as ex:
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
assert ex.value.code == 2
|
|
||||||
|
|
||||||
# Simulate import error
|
|
||||||
monkeypatch.setitem(sys.modules, "lxml", None)
|
|
||||||
monkeypatch.setattr("sys.hexversion", 0x0)
|
|
||||||
monkeypatch.setattr("nw.CONFIG.verQtValue", 50000)
|
|
||||||
monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000)
|
|
||||||
with pytest.raises(SystemExit) as ex:
|
|
||||||
nwGUI = nw.main(
|
|
||||||
["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
|
||||||
)
|
|
||||||
nwGUI.closeMain()
|
|
||||||
nwGUI.close()
|
|
||||||
assert ex.value.code & 4 == 4 # Python version not satisfied
|
|
||||||
assert ex.value.code & 8 == 8 # Qt version not satisfied
|
|
||||||
assert ex.value.code & 16 == 16 # PyQt version not satisfied
|
|
||||||
assert ex.value.code & 32 == 32 # lxml package missing
|
|
||||||
monkeypatch.undo()
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
|
||||||
def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -113,7 +35,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
|
|
||||||
# Create new, save, close project
|
# Create new, save, close project
|
||||||
nwGUI.theProject.projTree.setSeed(42)
|
nwGUI.theProject.projTree.setSeed(42)
|
||||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
assert nwGUI.newProject({"projPath": fncDir})
|
||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
assert nwGUI.closeProject()
|
assert nwGUI.closeProject()
|
||||||
|
|
||||||
@@ -130,9 +52,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
assert not nwGUI.theProject.spellCheck
|
assert not nwGUI.theProject.spellCheck
|
||||||
|
|
||||||
# Check the files
|
# Check the files
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||||
testFile = os.path.join(nwTempGUI, "0_nwProject.nwx")
|
testFile = os.path.join(nwTempGUI, "0_nwProject.nwx")
|
||||||
refFile = os.path.join(nwRef, "gui", "0_nwProject.nwx")
|
refFile = os.path.join(refDir, "gui", "0_nwProject.nwx")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
@@ -140,7 +62,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
# qtbot.stopForInteraction()
|
# qtbot.stopForInteraction()
|
||||||
|
|
||||||
# Re-open project
|
# Re-open project
|
||||||
assert nwGUI.openProject(nwFuncTemp)
|
assert nwGUI.openProject(fncDir)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
# Check that we loaded the data
|
# Check that we loaded the data
|
||||||
@@ -148,8 +70,8 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
assert len(nwGUI.theProject.projTree._treeOrder) == 8
|
assert len(nwGUI.theProject.projTree._treeOrder) == 8
|
||||||
assert len(nwGUI.theProject.projTree._treeRoots) == 4
|
assert len(nwGUI.theProject.projTree._treeRoots) == 4
|
||||||
assert nwGUI.theProject.projTree.trashRoot() is None
|
assert nwGUI.theProject.projTree.trashRoot() is None
|
||||||
assert nwGUI.theProject.projPath == nwFuncTemp
|
assert nwGUI.theProject.projPath == fncDir
|
||||||
assert nwGUI.theProject.projMeta == os.path.join(nwFuncTemp, "meta")
|
assert nwGUI.theProject.projMeta == os.path.join(fncDir, "meta")
|
||||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||||
assert nwGUI.theProject.projName == "New Project"
|
assert nwGUI.theProject.projName == "New Project"
|
||||||
assert nwGUI.theProject.bookTitle == ""
|
assert nwGUI.theProject.bookTitle == ""
|
||||||
@@ -379,33 +301,33 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
assert nwGUI.saveProject()
|
assert nwGUI.saveProject()
|
||||||
|
|
||||||
# Check the files
|
# Check the files
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||||
testFile = os.path.join(nwTempGUI, "1_nwProject.nwx")
|
testFile = os.path.join(nwTempGUI, "1_nwProject.nwx")
|
||||||
refFile = os.path.join(nwRef, "gui", "1_nwProject.nwx")
|
refFile = os.path.join(refDir, "gui", "1_nwProject.nwx")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||||
|
|
||||||
projFile = os.path.join(nwFuncTemp, "content", "031b4af5197ec.nwd")
|
projFile = os.path.join(fncDir, "content", "031b4af5197ec.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd")
|
testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "1_031b4af5197ec.nwd")
|
refFile = os.path.join(refDir, "gui", "1_031b4af5197ec.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwFuncTemp, "content", "1a6562590ef19.nwd")
|
projFile = os.path.join(fncDir, "content", "1a6562590ef19.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd")
|
testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "1_1a6562590ef19.nwd")
|
refFile = os.path.join(refDir, "gui", "1_1a6562590ef19.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
|
projFile = os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd")
|
testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd")
|
refFile = os.path.join(refDir, "gui", "1_0e17daca5f3e1.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
projFile = os.path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd")
|
projFile = os.path.join(fncDir, "content", "41cfc0d1f2d12.nwd")
|
||||||
testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd")
|
testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd")
|
||||||
refFile = os.path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd")
|
refFile = os.path.join(refDir, "gui", "1_41cfc0d1f2d12.nwd")
|
||||||
copyfile(projFile, testFile)
|
copyfile(projFile, testFile)
|
||||||
assert cmpFiles(testFile, refFile)
|
assert cmpFiles(testFile, refFile)
|
||||||
|
|
||||||
@@ -414,9 +336,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
|
def testDocViewer(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -566,9 +488,9 @@ def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
|
def testProjectTree(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -690,9 +612,9 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -927,9 +849,9 @@ def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
def testContextMenu(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1013,15 +935,15 @@ def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
def testInsertMenu(qtbot, monkeypatch, fncDir, tmpDir):
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
qtbot.wait(stepDelay)
|
qtbot.wait(stepDelay)
|
||||||
|
|
||||||
nwGUI.theProject.projTree.setSeed(42)
|
nwGUI.theProject.projTree.setSeed(42)
|
||||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
assert nwGUI.newProject({"projPath": fncDir})
|
||||||
|
|
||||||
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
|
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
|
||||||
|
|
||||||
@@ -1162,7 +1084,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
|||||||
assert not nwGUI.importDocument()
|
assert not nwGUI.importDocument()
|
||||||
|
|
||||||
# Then a valid path, but bot a file that exists
|
# Then a valid path, but bot a file that exists
|
||||||
theFile = os.path.join(nwTemp, "import.txt")
|
theFile = os.path.join(tmpDir, "import.txt")
|
||||||
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, ""))
|
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, ""))
|
||||||
assert not nwGUI.importDocument()
|
assert not nwGUI.importDocument()
|
||||||
|
|
||||||
@@ -1205,16 +1127,16 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
|||||||
assert len(theBits) == 3
|
assert len(theBits) == 3
|
||||||
assert theBits[0] == "File details for the currently open file"
|
assert theBits[0] == "File details for the currently open file"
|
||||||
assert theBits[1] == "Handle: 0e17daca5f3e1"
|
assert theBits[1] == "Handle: 0e17daca5f3e1"
|
||||||
assert theBits[2] == "Location: %s" % os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
|
assert theBits[2] == "Location: %s" % os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
|
||||||
|
|
||||||
# qtbot.stopForInteraction()
|
# qtbot.stopForInteraction()
|
||||||
nwGUI.closeMain()
|
nwGUI.closeMain()
|
||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
|
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1392,9 +1314,9 @@ def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
|
def testOutline(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1453,9 +1375,9 @@ def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
|
|||||||
nwGUI.close()
|
nwGUI.close()
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
|
def testThemes(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||||
|
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
qtbot.waitForWindowShown(nwGUI)
|
qtbot.waitForWindowShown(nwGUI)
|
||||||
@@ -1478,7 +1400,7 @@ def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
|
|||||||
|
|
||||||
# Re-open
|
# Re-open
|
||||||
assert nw.CONFIG.confPath == nwMinimal
|
assert nw.CONFIG.confPath == nwMinimal
|
||||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||||
assert nwGUI.mainConf.confPath == nwMinimal
|
assert nwGUI.mainConf.confPath == nwMinimal
|
||||||
qtbot.addWidget(nwGUI)
|
qtbot.addWidget(nwGUI)
|
||||||
nwGUI.show()
|
nwGUI.show()
|
||||||
@@ -1,679 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""novelWriter Project Class Tester
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
|
|
||||||
from shutil import copyfile
|
|
||||||
from zipfile import ZipFile
|
|
||||||
|
|
||||||
from nwtools import cmpFiles
|
|
||||||
|
|
||||||
from nw.core.project import NWProject
|
|
||||||
from nw.core.document import NWDoc
|
|
||||||
from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple
|
|
||||||
from nw.constants import nwConst, nwItemClass, nwItemType, nwItemLayout, nwFiles
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy):
|
|
||||||
"""Test that a basic project can be created, and opened and saved.
|
|
||||||
"""
|
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
|
||||||
testFile = os.path.join(nwTempProj, "1_nwProject.nwx")
|
|
||||||
refFile = os.path.join(nwRef, "proj", "1_nwProject.nwx")
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
|
|
||||||
# Setting no data should fail
|
|
||||||
assert not theProject.newProject({})
|
|
||||||
|
|
||||||
# Try again with a proper path
|
|
||||||
assert theProject.newProject({"projPath": nwFuncTemp})
|
|
||||||
assert theProject.setProjectPath(nwFuncTemp)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
# Creating the project once more should fail
|
|
||||||
assert not theProject.newProject({"projPath": nwFuncTemp})
|
|
||||||
|
|
||||||
# Check the new project
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
|
|
||||||
# Open again
|
|
||||||
assert theProject.openProject(projFile)
|
|
||||||
|
|
||||||
# Save and close
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
assert not theProject.projChanged
|
|
||||||
|
|
||||||
# Open a second time
|
|
||||||
assert theProject.openProject(projFile)
|
|
||||||
assert not theProject.openProject(projFile)
|
|
||||||
assert theProject.openProject(projFile, overrideLock=True)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
|
||||||
"""Check that new root folders can be added to the project.
|
|
||||||
"""
|
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
|
||||||
testFile = os.path.join(nwTempProj, "2_nwProject.nwx")
|
|
||||||
refFile = os.path.join(nwRef, "proj", "2_nwProject.nwx")
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
|
|
||||||
assert theProject.newProject({"projPath": nwFuncTemp})
|
|
||||||
assert theProject.setProjectPath(nwFuncTemp)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
assert theProject.openProject(projFile)
|
|
||||||
|
|
||||||
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
|
|
||||||
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
|
|
||||||
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
|
|
||||||
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
|
|
||||||
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
|
|
||||||
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
|
|
||||||
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
|
|
||||||
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
|
|
||||||
|
|
||||||
assert theProject.projChanged
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
assert not theProject.projChanged
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
|
||||||
"""Check that new files can be added to the project.
|
|
||||||
"""
|
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
|
||||||
testFile = os.path.join(nwTempProj, "3_nwProject.nwx")
|
|
||||||
refFile = os.path.join(nwRef, "proj", "3_nwProject.nwx")
|
|
||||||
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
|
|
||||||
assert theProject.newProject({"projPath": nwFuncTemp})
|
|
||||||
assert theProject.setProjectPath(nwFuncTemp)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
assert theProject.openProject(projFile)
|
|
||||||
|
|
||||||
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
|
|
||||||
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
|
|
||||||
assert theProject.projChanged
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
assert not theProject.projChanged
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
|
||||||
"""Create a new project from a project wizard dictionary.
|
|
||||||
Custom type with chapters and scenes.
|
|
||||||
"""
|
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
|
||||||
testFile = os.path.join(nwTempProj, "4_nwProject.nwx")
|
|
||||||
refFile = os.path.join(nwRef, "proj", "4_nwProject.nwx")
|
|
||||||
|
|
||||||
projData = {
|
|
||||||
"projName": "Test Custom",
|
|
||||||
"projTitle": "Test Novel",
|
|
||||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
|
||||||
"projPath": nwFuncTemp,
|
|
||||||
"popSample": False,
|
|
||||||
"popMinimal": False,
|
|
||||||
"popCustom": True,
|
|
||||||
"addRoots": [
|
|
||||||
nwItemClass.PLOT,
|
|
||||||
nwItemClass.CHARACTER,
|
|
||||||
nwItemClass.WORLD,
|
|
||||||
nwItemClass.TIMELINE,
|
|
||||||
nwItemClass.OBJECT,
|
|
||||||
nwItemClass.ENTITY,
|
|
||||||
],
|
|
||||||
"numChapters": 3,
|
|
||||||
"numScenes": 3,
|
|
||||||
"chFolders": True,
|
|
||||||
}
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
|
|
||||||
assert theProject.newProject(projData)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
|
||||||
"""Create a new project from a project wizard dictionary.
|
|
||||||
Custom type without chapters, but with scenes.
|
|
||||||
"""
|
|
||||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
|
||||||
testFile = os.path.join(nwTempProj, "5_nwProject.nwx")
|
|
||||||
refFile = os.path.join(nwRef, "proj", "5_nwProject.nwx")
|
|
||||||
|
|
||||||
projData = {
|
|
||||||
"projName": "Test Custom",
|
|
||||||
"projTitle": "Test Novel",
|
|
||||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
|
||||||
"projPath": nwFuncTemp,
|
|
||||||
"popSample": False,
|
|
||||||
"popMinimal": False,
|
|
||||||
"popCustom": True,
|
|
||||||
"addRoots": [
|
|
||||||
nwItemClass.PLOT,
|
|
||||||
nwItemClass.CHARACTER,
|
|
||||||
nwItemClass.WORLD,
|
|
||||||
nwItemClass.TIMELINE,
|
|
||||||
nwItemClass.OBJECT,
|
|
||||||
nwItemClass.ENTITY,
|
|
||||||
],
|
|
||||||
"numChapters": 0,
|
|
||||||
"numScenes": 6,
|
|
||||||
"chFolders": True,
|
|
||||||
}
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
|
|
||||||
assert theProject.newProject(projData)
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
copyfile(projFile, testFile)
|
|
||||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp):
|
|
||||||
"""Check that we can create a new project can be created from the
|
|
||||||
provided sample project via a zip file.
|
|
||||||
"""
|
|
||||||
projData = {
|
|
||||||
"projName": "Test Sample",
|
|
||||||
"projTitle": "Test Novel",
|
|
||||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
|
||||||
"projPath": nwFuncTemp,
|
|
||||||
"popSample": True,
|
|
||||||
"popMinimal": False,
|
|
||||||
"popCustom": False,
|
|
||||||
}
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
theProject.mainConf = nwConf
|
|
||||||
|
|
||||||
# Sample set, but no path
|
|
||||||
assert not theProject.newProject({"popSample": True})
|
|
||||||
|
|
||||||
# Force the lookup path for assets to our temp folder
|
|
||||||
srcSample = os.path.abspath(os.path.join(nwConf.appRoot, "sample"))
|
|
||||||
dstSample = os.path.join(nwTemp, "sample.zip")
|
|
||||||
nwConf.assetPath = nwTemp
|
|
||||||
|
|
||||||
# Create and open a defective zip file
|
|
||||||
with open(dstSample, mode="w+") as outFile:
|
|
||||||
outFile.write("foo")
|
|
||||||
|
|
||||||
assert not theProject.newProject(projData)
|
|
||||||
os.unlink(dstSample)
|
|
||||||
|
|
||||||
# Create a real zip file, and unpack it
|
|
||||||
with ZipFile(dstSample, "w") as zipObj:
|
|
||||||
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
|
|
||||||
for docFile in os.listdir(os.path.join(srcSample, "content")):
|
|
||||||
srcDoc = os.path.join(srcSample, "content", docFile)
|
|
||||||
zipObj.write(srcDoc, "content/"+docFile)
|
|
||||||
|
|
||||||
assert theProject.newProject(projData)
|
|
||||||
assert theProject.openProject(nwFuncTemp)
|
|
||||||
assert theProject.projName == "Sample Project"
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
os.unlink(dstSample)
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectNewSampleB(monkeypatch, nwFuncTemp, nwConf, nwDummy, nwTemp):
|
|
||||||
"""Check that we can create a new project can be created from the
|
|
||||||
provided sample project folder.
|
|
||||||
"""
|
|
||||||
projData = {
|
|
||||||
"projName": "Test Sample",
|
|
||||||
"projTitle": "Test Novel",
|
|
||||||
"projAuthors": "Jane Doe\nJohn Doh\n",
|
|
||||||
"projPath": nwFuncTemp,
|
|
||||||
"popSample": True,
|
|
||||||
"popMinimal": False,
|
|
||||||
"popCustom": False,
|
|
||||||
}
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
theProject.mainConf = nwConf
|
|
||||||
|
|
||||||
# Make sure we do not pick up the nw/assets/sample.zip file
|
|
||||||
nwConf.assetPath = nwTemp
|
|
||||||
|
|
||||||
# Set a fake project file name
|
|
||||||
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx")
|
|
||||||
assert not theProject.newProject(projData)
|
|
||||||
|
|
||||||
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx")
|
|
||||||
assert theProject.newProject(projData)
|
|
||||||
assert theProject.openProject(nwFuncTemp)
|
|
||||||
assert theProject.projName == "Sample Project"
|
|
||||||
assert theProject.saveProject()
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
# Misdirect the appRoot path so neither is possible
|
|
||||||
nwConf.appRoot = nwTemp
|
|
||||||
assert not theProject.newProject(projData)
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectMethods(monkeypatch, nwMinimal, nwDummy):
|
|
||||||
"""Test other project class methods and functions.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
assert theProject.openProject(nwMinimal)
|
|
||||||
assert theProject.projPath == nwMinimal
|
|
||||||
|
|
||||||
# Setting project path
|
|
||||||
assert theProject.setProjectPath(None)
|
|
||||||
assert theProject.projPath is None
|
|
||||||
assert theProject.setProjectPath("")
|
|
||||||
assert theProject.projPath is None
|
|
||||||
assert theProject.setProjectPath("~")
|
|
||||||
assert theProject.projPath == os.path.expanduser("~")
|
|
||||||
|
|
||||||
# Create a new folder and populate it
|
|
||||||
projPath = os.path.join(nwMinimal, "dummy1")
|
|
||||||
assert theProject.setProjectPath(projPath, newProject=True)
|
|
||||||
|
|
||||||
# Make os.mkdir fail
|
|
||||||
def altMkdir(*args):
|
|
||||||
raise Exception("Oops!")
|
|
||||||
|
|
||||||
monkeypatch.setattr("os.mkdir", altMkdir)
|
|
||||||
projPath = os.path.join(nwMinimal, "dummy2")
|
|
||||||
assert not theProject.setProjectPath(projPath, newProject=True)
|
|
||||||
|
|
||||||
# Project Name
|
|
||||||
assert theProject.setProjectName(" A Name ")
|
|
||||||
assert theProject.projName == "A Name"
|
|
||||||
|
|
||||||
# Project Title
|
|
||||||
assert theProject.setBookTitle(" A Title ")
|
|
||||||
assert theProject.bookTitle == "A Title"
|
|
||||||
|
|
||||||
# Project Authors
|
|
||||||
assert not theProject.setBookAuthors([])
|
|
||||||
assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ")
|
|
||||||
assert theProject.bookAuthors == ["Jane Doe", "John Doh"]
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testDocMeta(nwDummy, nwLipsum):
|
|
||||||
"""Check that the document meta data string is parsed correctly.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.projTree.setSeed(42)
|
|
||||||
assert theProject.openProject(nwLipsum)
|
|
||||||
|
|
||||||
aDoc = NWDoc(theProject, nwDummy)
|
|
||||||
assert aDoc.openDocument("47666c91c7ccf")
|
|
||||||
theName, theParent, theClass, theLayout = aDoc.getMeta()
|
|
||||||
|
|
||||||
assert theName == "Scene Five"
|
|
||||||
assert theParent == "6bd935d2490cd"
|
|
||||||
assert theClass == nwItemClass.NOVEL
|
|
||||||
assert theLayout == nwItemLayout.SCENE
|
|
||||||
|
|
||||||
aDoc._docMeta = {"stuff": None}
|
|
||||||
theName, theParent, theClass, theLayout = aDoc.getMeta()
|
|
||||||
assert theName == ""
|
|
||||||
assert theParent is None
|
|
||||||
assert theClass is None
|
|
||||||
assert theLayout is None
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testSpellEnchant(nwTemp, nwConf):
|
|
||||||
wList = os.path.join(nwTemp, "wordlist.txt")
|
|
||||||
with open(wList, mode="w") as wFile:
|
|
||||||
wFile.write("a_word\nb_word\nc_word\n")
|
|
||||||
|
|
||||||
spChk = NWSpellEnchant()
|
|
||||||
spChk.mainConf = nwConf
|
|
||||||
spChk.setLanguage("en", wList)
|
|
||||||
|
|
||||||
assert spChk.checkWord("a_word")
|
|
||||||
assert spChk.checkWord("b_word")
|
|
||||||
assert spChk.checkWord("c_word")
|
|
||||||
assert not spChk.checkWord("d_word")
|
|
||||||
|
|
||||||
spChk.addWord("d_word")
|
|
||||||
assert spChk.checkWord("d_word")
|
|
||||||
|
|
||||||
wSuggest = spChk.suggestWords("wrod")
|
|
||||||
assert len(wSuggest) > 0
|
|
||||||
assert "word" in wSuggest
|
|
||||||
|
|
||||||
dList = spChk.listDictionaries()
|
|
||||||
assert len(dList) > 0
|
|
||||||
|
|
||||||
aTag, aName = spChk.describeDict()
|
|
||||||
assert aTag == "en"
|
|
||||||
assert aName != ""
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testSpellSimple(nwTemp, nwConf):
|
|
||||||
wList = os.path.join(nwTemp, "wordlist.txt")
|
|
||||||
with open(wList, mode="w") as wFile:
|
|
||||||
wFile.write("a_word\nb_word\nc_word\n")
|
|
||||||
|
|
||||||
spChk = NWSpellSimple()
|
|
||||||
spChk.mainConf = nwConf
|
|
||||||
spChk.setLanguage("en", wList)
|
|
||||||
|
|
||||||
assert spChk.checkWord("a_word")
|
|
||||||
assert spChk.checkWord("b_word")
|
|
||||||
assert spChk.checkWord("c_word")
|
|
||||||
assert not spChk.checkWord("d_word")
|
|
||||||
|
|
||||||
spChk.addWord("d_word")
|
|
||||||
assert spChk.checkWord("d_word")
|
|
||||||
|
|
||||||
wSuggest = spChk.suggestWords("wrod")
|
|
||||||
assert len(wSuggest) > 0
|
|
||||||
assert "word" in wSuggest
|
|
||||||
|
|
||||||
dList = spChk.listDictionaries()
|
|
||||||
assert len(dList) > 0
|
|
||||||
|
|
||||||
aTag, aName = spChk.describeDict()
|
|
||||||
assert aTag == "en"
|
|
||||||
assert aName == nwConst.SP_INTERNAL
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectOptions(nwDummy, nwLipsum):
|
|
||||||
"""Test the class that holds all the GUI state user options that are
|
|
||||||
tied to the current open project. Non-project related GUI options
|
|
||||||
are handled by the Config class.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
assert theProject.projMeta is None
|
|
||||||
|
|
||||||
theOpts = theProject.optState
|
|
||||||
assert not theOpts.loadSettings()
|
|
||||||
assert not theOpts.saveSettings()
|
|
||||||
|
|
||||||
# No Settings
|
|
||||||
assert theProject.openProject(nwLipsum)
|
|
||||||
assert theOpts.loadSettings()
|
|
||||||
assert theOpts.saveSettings()
|
|
||||||
assert str(theOpts.theState) == r"{}"
|
|
||||||
|
|
||||||
# Read Invalid Settings and Filter
|
|
||||||
stateFile = os.path.join(theProject.projMeta, nwFiles.OPTS_FILE)
|
|
||||||
with open(stateFile, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write(
|
|
||||||
r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}'
|
|
||||||
)
|
|
||||||
assert theOpts.loadSettings()
|
|
||||||
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 100, 'winHeight': 50}}"
|
|
||||||
|
|
||||||
# Set New Settings
|
|
||||||
assert not theOpts.setValue("NoGroup", "NoName", None)
|
|
||||||
assert not theOpts.setValue("GuiProjectSettings", "NoName", None)
|
|
||||||
assert theOpts.setValue("GuiProjectSettings", "winWidth", 200)
|
|
||||||
assert theOpts.setValue("GuiProjectSettings", "winHeight", 80)
|
|
||||||
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 200, 'winHeight': 80}}"
|
|
||||||
|
|
||||||
# Check Read/Write Types
|
|
||||||
|
|
||||||
## String
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
|
||||||
assert isinstance(theOpts.getString("GuiWritingStats", "winWidth", "456"), str)
|
|
||||||
assert theOpts.getString("GuiWritingStats", "NoName", "456") == "456"
|
|
||||||
|
|
||||||
## Int
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
|
||||||
assert isinstance(theOpts.getInt("GuiWritingStats", "winWidth", 456), int)
|
|
||||||
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
|
||||||
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
|
|
||||||
|
|
||||||
## Float
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
|
||||||
assert isinstance(theOpts.getFloat("GuiWritingStats", "winWidth", 456.0), float)
|
|
||||||
assert theOpts.getFloat("GuiWritingStats", "NoName", 456.0) == 456.0
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
|
||||||
assert theOpts.getFloat("GuiWritingStats", "winWidth", 456.0) == 456.0
|
|
||||||
|
|
||||||
## Bool
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", True)
|
|
||||||
assert isinstance(theOpts.getBool("GuiWritingStats", "winWidth", False), bool)
|
|
||||||
assert theOpts.getFloat("GuiWritingStats", "NoName", False) is False
|
|
||||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
|
||||||
assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectOrphanedFiles(nwDummy, nwLipsum):
|
|
||||||
"""Check that files in the content folder that are not tracked in
|
|
||||||
the project XML file are handled correctly by the orphaned files
|
|
||||||
function. It should also restore as much meta data as possible from
|
|
||||||
the meta line at the top of the document file.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
assert theProject.openProject(nwLipsum)
|
|
||||||
assert theProject.projTree["636b6aa9b697b"] is None
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
# First Item with Meta Data
|
|
||||||
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
|
|
||||||
with open(orphPath, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("%%~name:Mars\n")
|
|
||||||
outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n")
|
|
||||||
outFile.write("%%~kind:WORLD/NOTE\n")
|
|
||||||
outFile.write("%%~invalid\n")
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
# Second Item without Meta Data
|
|
||||||
orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
|
|
||||||
with open(orphPath, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
# Invalid File Name
|
|
||||||
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt")
|
|
||||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
# Invalid File Name
|
|
||||||
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
|
|
||||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
# Invalid File Name
|
|
||||||
dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd")
|
|
||||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
assert theProject.openProject(nwLipsum)
|
|
||||||
assert theProject.projPath is not None
|
|
||||||
assert theProject.projTree["636b6aa9b697bb"] is None
|
|
||||||
assert theProject.projTree["abcdefghijklm"] is None
|
|
||||||
|
|
||||||
# First Item with Meta Data
|
|
||||||
oItem = theProject.projTree["636b6aa9b697b"]
|
|
||||||
assert oItem is not None
|
|
||||||
assert oItem.itemName == "Mars"
|
|
||||||
assert oItem.itemHandle == "636b6aa9b697b"
|
|
||||||
assert oItem.itemParent is None
|
|
||||||
assert oItem.itemClass == nwItemClass.WORLD
|
|
||||||
assert oItem.itemType == nwItemType.FILE
|
|
||||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
|
||||||
|
|
||||||
# Second Item without Meta Data
|
|
||||||
oItem = theProject.projTree["736b6aa9b697b"]
|
|
||||||
assert oItem is not None
|
|
||||||
assert oItem.itemName == "Orphaned File 1"
|
|
||||||
assert oItem.itemHandle == "736b6aa9b697b"
|
|
||||||
assert oItem.itemParent is None
|
|
||||||
assert oItem.itemClass == nwItemClass.NO_CLASS
|
|
||||||
assert oItem.itemType == nwItemType.FILE
|
|
||||||
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
|
|
||||||
|
|
||||||
assert theProject.saveProject(nwLipsum)
|
|
||||||
assert theProject.closeProject()
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectOldFormat(nwDummy, nwOldProj):
|
|
||||||
"""Test that a project folder structure of version 1.0 can be
|
|
||||||
converted to the latest folder structure. Version 1.0 split the
|
|
||||||
documents into 'data_0' ... 'data_f' folders, which are now all
|
|
||||||
contained in a single 'content' folder.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
theProject.mainConf.showGUI = False
|
|
||||||
|
|
||||||
# Create dummy files for known legacy files
|
|
||||||
deleteFiles = [
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.0"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.1"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.2"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.3"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.4"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.5"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.6"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.7"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.8"),
|
|
||||||
os.path.join(nwOldProj, "cache", "nwProject.nwx.9"),
|
|
||||||
os.path.join(nwOldProj, "meta", "mainOptions.json"),
|
|
||||||
os.path.join(nwOldProj, "meta", "exportOptions.json"),
|
|
||||||
os.path.join(nwOldProj, "meta", "outlineOptions.json"),
|
|
||||||
os.path.join(nwOldProj, "meta", "timelineOptions.json"),
|
|
||||||
os.path.join(nwOldProj, "meta", "docMergeOptions.json"),
|
|
||||||
os.path.join(nwOldProj, "meta", "sessionLogOptions.json"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add some files that shouldn't be there
|
|
||||||
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd"))
|
|
||||||
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt"))
|
|
||||||
|
|
||||||
# Add some folders that shouldn't be there
|
|
||||||
os.mkdir(os.path.join(nwOldProj, "stuff"))
|
|
||||||
os.mkdir(os.path.join(nwOldProj, "data_1", "stuff"))
|
|
||||||
|
|
||||||
# Create dummy files
|
|
||||||
os.mkdir(os.path.join(nwOldProj, "cache"))
|
|
||||||
for aFile in deleteFiles:
|
|
||||||
with open(aFile, mode="w+", encoding="utf8") as outFile:
|
|
||||||
outFile.write("Hi")
|
|
||||||
for aFile in deleteFiles:
|
|
||||||
assert os.path.isfile(aFile)
|
|
||||||
|
|
||||||
# Open project and check that files that are not supposed to be
|
|
||||||
# there have been removed
|
|
||||||
assert theProject.openProject(nwOldProj)
|
|
||||||
for aFile in deleteFiles:
|
|
||||||
assert not os.path.isfile(aFile)
|
|
||||||
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_1"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_7"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_8"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_9"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_a"))
|
|
||||||
assert not os.path.isdir(os.path.join(nwOldProj, "data_f"))
|
|
||||||
|
|
||||||
# Check stuff that has been moved
|
|
||||||
assert os.path.isdir(os.path.join(nwOldProj, "junk"))
|
|
||||||
assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt"))
|
|
||||||
|
|
||||||
# Check that files we want to keep are in the right place
|
|
||||||
assert os.path.isdir(os.path.join(nwOldProj, "cache"))
|
|
||||||
assert os.path.isdir(os.path.join(nwOldProj, "content"))
|
|
||||||
assert os.path.isdir(os.path.join(nwOldProj, "meta"))
|
|
||||||
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd"))
|
|
||||||
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log"))
|
|
||||||
|
|
||||||
# Close the project
|
|
||||||
theProject.closeProject()
|
|
||||||
|
|
||||||
# Check that new files have been created
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log"))
|
|
||||||
assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt"))
|
|
||||||
|
|
||||||
@pytest.mark.project
|
|
||||||
def testProjectBackup(nwDummy, nwMinimal, nwTemp):
|
|
||||||
"""Test the automated backup feature of the project class. The test
|
|
||||||
creates a backup of the Minimal test project, and then unzips the
|
|
||||||
backupd file and checks that the project XML file is identical to
|
|
||||||
the original file.
|
|
||||||
"""
|
|
||||||
theProject = NWProject(nwDummy)
|
|
||||||
assert theProject.openProject(nwMinimal)
|
|
||||||
|
|
||||||
# Test faulty settings
|
|
||||||
# Invalid path
|
|
||||||
theProject.mainConf.backupPath = None
|
|
||||||
assert not theProject.zipIt(doNotify=False)
|
|
||||||
|
|
||||||
# Missing project name
|
|
||||||
theProject.mainConf.backupPath = nwTemp
|
|
||||||
theProject.projName = ""
|
|
||||||
assert not theProject.zipIt(doNotify=False)
|
|
||||||
|
|
||||||
# Non-existent folder
|
|
||||||
theProject.mainConf.backupPath = os.path.join(nwTemp, "nonexistent")
|
|
||||||
theProject.projName = "Test Minimal"
|
|
||||||
assert not theProject.zipIt(doNotify=False)
|
|
||||||
|
|
||||||
# Same folder as project (causes infinite loop in zipping)
|
|
||||||
theProject.mainConf.backupPath = nwMinimal
|
|
||||||
assert not theProject.zipIt(doNotify=False)
|
|
||||||
|
|
||||||
# Test correct settings
|
|
||||||
theProject.mainConf.backupPath = nwTemp
|
|
||||||
assert theProject.zipIt(doNotify=False)
|
|
||||||
|
|
||||||
theFiles = os.listdir(os.path.join(nwTemp, "Test Minimal"))
|
|
||||||
assert len(theFiles) == 1
|
|
||||||
|
|
||||||
theZip = theFiles[0]
|
|
||||||
assert theZip[:12] == "Backup from "
|
|
||||||
assert theZip[-4:] == ".zip"
|
|
||||||
|
|
||||||
# Extract the archive
|
|
||||||
with ZipFile(os.path.join(nwTemp, "Test Minimal", theZip), "r") as inZip:
|
|
||||||
inZip.extractall(os.path.join(nwTemp, "extract"))
|
|
||||||
|
|
||||||
# Check that the main project file was restored
|
|
||||||
assert cmpFiles(
|
|
||||||
os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx")
|
|
||||||
)
|
|
||||||
@@ -70,3 +70,15 @@ def getGuiItem(theName):
|
|||||||
if qWidget.objectName() == theName:
|
if qWidget.objectName() == theName:
|
||||||
return qWidget
|
return qWidget
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def readFile(fileName):
|
||||||
|
"""Returns the content of a file as a string.
|
||||||
|
"""
|
||||||
|
with open(fileName, mode="r", encoding="utf8") as inFile:
|
||||||
|
return inFile.read()
|
||||||
|
|
||||||
|
def writeFile(fileName, fileData):
|
||||||
|
"""Write the contents of a string to a file.
|
||||||
|
"""
|
||||||
|
with open(fileName, mode="w", encoding="utf8") as outFile:
|
||||||
|
outFile.write(fileData)
|
||||||