Merge pull request #206 from vkbo/dev_0.6

Large GUI changes ready for release 0.6
This commit is contained in:
Veronica K. Berglyd Olsen
2020-05-21 20:47:50 +02:00
committed by GitHub
116 changed files with 4622 additions and 3035 deletions
+10
View File
@@ -1,5 +1,15 @@
# novelWriter ChangeLog
## Version 0.6 [2020-xx-xx]
**User Interface**
* The Export Tool has been removed and replaced by a new tool called "Build Novel Project". The new tool has the same filtering options as the Export Tool, but with more formatting options for titles. It also has a preview window to display the generated document. A Save As button provides exports to HTML, novelWriter Markdown. plain text, PDF and Open Document format. LaTeX export has not been ported over, and interfacing with Pandoc is no longer supported either. Although, as before, the HTML export can be converted with Pandoc to other formats outside of novelWriter. The new tool also supports printing. PR #204
* The Project Settings, Preferences, Item Editor, Merge Documents, and Split Documents dialogs have been redesigned. The ones with tabs now have vertical tabs on the left with horizontal labels. The dialog design should be more compact, and have room for more tabs for future settings. PR #212
* A new icon, as well as a mimetype icon for the project files, have been designed and added to the app. PRs #213 and #214
* The About dialog has been completely redesigned to allow more information. PR #217
* The Open Project dialog has been cleaned up and made more readable. The project paths have been moved out of the list, and are now displayed when an item is selected instead. Icons have been added, and the New project dialog can also be triggered from this dialog. PR #218
## Version 0.5.2 [2020-05-21]
**Bugfixes**
+9 -2
View File
@@ -4,9 +4,16 @@
[![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/master/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter)
[![Documentation Status](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
<img align="left" style="margin: 0 16px 4px 0;" src="assets/icons/96x96/novelwriter.png">
novelWriter is a markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents.
The documentation is available here: [novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/).
novelWriter uses its own flavour of markdown that supports a meta data syntax for comments, synopsis and cross-referencing between files.
novelWriter is licensed under GPLv3, see the [GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details, or consult the [LICENSE](LICENSE.md) file.
The bundled Typicon-based icon themes are licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/).
Below is a short overview of the features of novelWriter.
The full documentation is available at [novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/).
### Note
@@ -174,4 +181,4 @@ They broadly follow Python PEP8, but there are a few modifications.
![Screenshot 1](docs/source/images/screenshot_default.png)
**novelWriter with dark theme:**
![Screenshot 1](docs/source/images/screenshot_dark.png)
![Screenshot 2](docs/source/images/screenshot_dark.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+224
View File
@@ -0,0 +1,224 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="96.000008"
inkscape:export-xdpi="96.000008"
inkscape:export-filename="/home/vkbo/Code/novelWriter/Icons/novelWriter/Version 3/icons/1024x1024/novelwriter.png"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="icon.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient1470"
inkscape:collect="always">
<stop
id="stop1466"
offset="0"
style="stop-color:#7a9cb8;stop-opacity:1" />
<stop
id="stop1468"
offset="1"
style="stop-color:#5978a6;stop-opacity:1" />
</linearGradient>
<rect
id="rect852"
height="65.785888"
width="130.57787"
y="205.79962"
x="60.484347" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1474" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect852-7" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1544" />
<rect
id="rect1570"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1587"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<filter
id="filter1444"
inkscape:label="Drop Shadow"
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"
inkscape:label="Drop Shadow"
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
gradientTransform="matrix(2.2584252,1.5089457,-0.74487144,1.1148423,-169.6027,-163.93817)"
gradientUnits="userSpaceOnUse"
r="130.96873"
fy="67.111206"
fx="111.19364"
cy="67.111206"
cx="111.19364"
id="radialGradient1474"
xlink:href="#linearGradient1470"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="190.20708"
inkscape:cy="572.85481"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="2560"
inkscape:window-height="1344"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<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
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="display:inline;fill:url(#radialGradient1474);fill-opacity:1;stroke:#517995;stroke-width:2.64583;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1467"
width="259.29166"
height="259.29166"
x="5.8208332"
y="5.8208332"
rx="21.166666"
ry="21.166666"
inkscape:label="Box" />
</g>
<g
style="display:inline"
inkscape:label="Logo"
id="layer2"
inkscape:groupmode="layer">
<path
transform="scale(0.26458333)"
d="m 312,200 c -44.32,0 -80,35.68 -80,80 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.68,80 80,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 762.83789 H 792 V 480.00195 a 279.99999,279.99999 0 0 0 -280,-280 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 C 363.30011,213.54621 339.29632,200 312,200 Z m 231.46094,563.22656 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"
inkscape:label="n"
style="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)"
id="rect1406" />
<path
transform="scale(0.26458333)"
d="m 632,762.83789 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 884.67773,775.08008 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 792 v -0.16406 z"
inkscape:label="Arrow"
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)"
id="path897" />
<rect
inkscape:label="Highlight"
ry="5.2916665"
rx="5.2916665"
y="67.997917"
x="69.849998"
height="119.0625"
width="10.583333"
id="rect914"
style="fill:#c32222;fill-opacity:1;stroke:none;stroke-width:2.64583;stroke-linejoin:round;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

+252
View File
@@ -0,0 +1,252 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="3"
inkscape:export-xdpi="3"
inkscape:export-filename="/home/vkbo/Code/novelWriter/Icons/novelWriter-32.png"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="mimetype-nwx.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient2515"
inkscape:collect="always">
<stop
id="stop2511"
offset="0"
style="stop-color:#f7f7f7;stop-opacity:1" />
<stop
id="stop2513"
offset="1"
style="stop-color:#e5e5e5;stop-opacity:1" />
</linearGradient>
<rect
id="rect852"
height="65.785888"
width="130.57787"
y="205.79962"
x="60.484347" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1474" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect852-7" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1544" />
<rect
id="rect1570"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1587"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter1444">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1434" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1436" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1438" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1440" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1442" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter1456">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1446" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1448" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1450" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1452" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1454" />
</filter>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
r="410"
fy="623.97186"
fx="605.42725"
cy="623.97186"
cx="605.42725"
id="radialGradient2517"
xlink:href="#linearGradient2515"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="505.1269"
inkscape:cy="568.88259"
inkscape:document-units="px"
inkscape:current-layer="layer3"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="2560"
inkscape:window-height="1344"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<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
style="display:inline"
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Sheet">
<path
id="rect1481"
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
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"
transform="scale(0.26458333)"
inkscape:label="Page"
clip-path="none" />
<path
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"
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"
id="path1490"
inkscape:label="Corner" />
<g
inkscape:label=".nwx"
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"
id="text850"
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
aria-label=".nwx">
<path
inkscape:label="dot"
id="path856"
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"
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" />
<path
inkscape:label="n"
id="path858"
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"
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" />
<path
inkscape:label="w"
id="path860"
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"
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" />
<path
inkscape:label="x"
id="path862"
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"
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" />
</g>
</g>
<g
inkscape:label="Logo"
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)"
id="g2509">
<path
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"
inkscape:label="n"
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)"
id="rect1406-1"
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)" />
<path
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"
inkscape:label="Arrow"
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)"
id="path897"
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)" />
<rect
inkscape:label="Highlight"
ry="3.6658845"
rx="3.3791983"
y="1.2969339"
x="-118.27303"
height="82.482399"
width="6.7583966"
id="rect914"
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
cd ..
EXEC=$(pwd)/novelWriter.py
EXEC=$(echo $EXEC | sed 's_/_\\/_g')
sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop
cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/
cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/
cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/
cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/
cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/
cp -v assets/icons/1024x1024/novelwriter.png /usr/share/icons/hicolor/1024x1024/apps/
cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/
cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg
cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/
update-mime-database /usr/share/mime/
update-icon-caches /usr/share/icons/hicolor/24x24/apps/
update-icon-caches /usr/share/icons/hicolor/48x48/apps/
update-icon-caches /usr/share/icons/hicolor/96x96/apps/
update-icon-caches /usr/share/icons/hicolor/256x256/apps/
update-icon-caches /usr/share/icons/hicolor/512x512/apps/
update-icon-caches /usr/share/icons/hicolor/1024x1024/apps/
update-icon-caches /usr/share/icons/hicolor/scalable/apps/
update-icon-caches /usr/share/icons/hicolor/scalable/mimetypes/
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-novelwriter-project">
<comment>novelWriter Project </comment>
<sub-class-of type="application/xml"/>
<glob pattern="*.nwx"/>
</mime-type>
</mime-info>
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Encoding=UTF-8
Name=novelWriter
Comment=Multi-document markdown editor for novels
Exec=%%exec%% %f
Icon=novelwriter
Categories=GTK;GNOME;Office;
Terminal=false
MimeType=application/x-novelwriter-project
+1
View File
@@ -47,6 +47,7 @@ __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__status__ = "Pre-Release"
__url__ = "https://github.com/vkbo/novelWriter"
__domain__ = "novelwriter.io"
__docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [
"Veronica Berglyd Olsen (developer)",
-44
View File
@@ -1,44 +0,0 @@
<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="18"
id="Layer_1"
version="1.2"
viewBox="0 0 19 18"
width="19"
xml:space="preserve"
sodipodi:docname="export.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata874"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs872" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1080"
id="namedview870"
showgrid="false"
inkscape:zoom="33.875"
inkscape:cx="12.118081"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><path
d="M 18.711,6.796 C 18.67,6.755 14.656,2.7 12.729,0.65 12.309,0.236 11.73,0 11.143,0 9.961,0 9,0.896 9,2 H 1 C 0.447,2 0,2.448 0,3 v 14 c 0,0.552 0.447,1 1,1 h 14 c 0.553,0 1,-0.448 1,-1 v -6.045 c 1.434,-1.461 2.688,-2.729 2.711,-2.751 0.387,-0.39 0.387,-1.018 0,-1.408 z m -7.432,6.145 C 11.257,12.968 11.214,13 11.143,13 11.069,13 11.022,12.977 10.999,12.96 V 10 9 h -1 C 8.228,9.034 6.663,9.68 5.246,10.958 5.676,8.743 6.846,6 9.999,6 h 1 V 2.042 C 11.027,2.021 11.07,2 11.143,2 c 0.09,0 0.152,0.05 0.154,0.05 1.436,1.525 4.051,4.187 5.297,5.45 -0.253,0.257 -4.342,4.422 -5.315,5.441 z M 2,16 V 4 h 8 c 0,0.348 0,0.695 0,1 C 5.34,5 4,9.871 4,13.5 V 14 c 1.691,-2.578 3.6,-3.953 6,-4 0,1.045 0,2.838 0,3 0,0.551 0.512,1 1.143,1 0.364,0 0.676,-0.158 0.883,-0.391 0.539,-0.565 1.242,-1.291 1.976,-2.043 V 16 Z"
id="path867"
style="fill:#000000;fill-opacity:0.35408559"
inkscape:connector-curvature="0" /></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

-48
View File
@@ -1,48 +0,0 @@
<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="280px"
height="279.416px"
viewBox="340 140 280 279.416"
enable-background="new 340 140 280 279.416"
xml:space="preserve"
sodipodi:docname="gear.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata9"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1013"
id="namedview5"
showgrid="false"
inkscape:zoom="0.84461882"
inkscape:cx="22.787362"
inkscape:cy="139.70799"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
d="M620,305.666v-51.333l-31.5-5.25c-2.333-8.75-5.833-16.917-9.917-23.917L597.25,199.5l-36.167-36.75l-26.25,18.083 c-7.583-4.083-15.75-7.583-23.916-9.917L505.667,140h-51.334l-5.25,31.5c-8.75,2.333-16.333,5.833-23.916,9.916L399.5,163.333 L362.75,199.5l18.667,25.666c-4.083,7.584-7.583,15.75-9.917,24.5l-31.5,4.667v51.333l31.5,5.25 c2.333,8.75,5.833,16.334,9.917,23.917l-18.667,26.25l36.167,36.167l26.25-18.667c7.583,4.083,15.75,7.583,24.5,9.917l5.25,30.916 h51.333l5.25-31.5c8.167-2.333,16.333-5.833,23.917-9.916l26.25,18.666l36.166-36.166l-18.666-26.25 c4.083-7.584,7.583-15.167,9.916-23.917L620,305.666z M480,333.666c-29.75,0-53.667-23.916-53.667-53.666s24.5-53.667,53.667-53.667 S533.667,250.25,533.667,280S509.75,333.666,480,333.666z"
id="path2"
style="fill:#000000;fill-opacity:0.35408559" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

-8
View File
@@ -1,8 +0,0 @@
FROM ICON SET: Typicons
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
https://creativecommons.org/licenses/by-sa/3.0/
Applies to:
* graphics/export.svg
* graphics/merge.svg
* graphics/split.svg
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
viewBox="0 0 20 20"
height="20"
width="20"
version="1.2">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="fill:#000000;fill-opacity:0.35294118;stroke:none;stroke-opacity:1"
id="path2"
d="m 16,5.816 v 1.851 c 0,1.93 -1.57,3.5 -3.5,3.5 -0.827,0 -1.5,0.673 -1.5,1.5 v 1.517 c 1.161,0.415 2,1.514 2,2.816 0,1.654 -1.346,3 -3,3 C 8.346,20 7,18.654 7,17 7,15.698 7.839,14.599 9,14.185 V 12.667 C 9,11.84 8.327,11.167 7.5,11.167 5.57,11.167 4,9.597 4,7.667 V 5.816 C 2.839,5.401 2,4.302 2,3 2,1.346 3.346,0 5,0 6.654,0 8,1.346 8,3 8,4.302 7.161,5.401 6,5.816 v 1.851 c 0,0.827 0.673,1.5 1.5,1.5 0.979,0 1.864,0.407 2.5,1.058 0.636,-0.651 1.521,-1.058 2.5,-1.058 0.827,0 1.5,-0.673 1.5,-1.5 V 5.816 C 12.839,5.401 12,4.302 12,3 c 0,-1.654 1.346,-3 3,-3 1.654,0 3,1.346 3,3 0,1.302 -0.839,2.401 -2,2.816 z M 5,2 C 4.448,2 4,2.449 4,3 4,3.551 4.448,4 5,4 5.552,4 6,3.551 6,3 6,2.449 5.552,2 5,2 Z m 5,16 c 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 z M 15,2 c -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
viewBox="0 0 20 20"
height="20"
width="20"
version="1.2">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="fill:#000000;fill-opacity:0.35294118;stroke:none;stroke-opacity:1"
id="path2"
d="m 16,14.184 v -1.851 c 0,-1.93 -1.57,-3.5 -3.5,-3.5 -0.827,0 -1.5,-0.673 -1.5,-1.5 V 5.816 C 12.161,5.401 13,4.302 13,3 13,1.346 11.654,0 10,0 8.346,0 7,1.346 7,3 7,4.302 7.839,5.401 9,5.815 v 1.518 c 0,0.827 -0.673,1.5 -1.5,1.5 -1.93,0 -3.5,1.57 -3.5,3.5 v 1.851 C 2.839,14.599 2,15.698 2,17 c 0,1.654 1.346,3 3,3 1.654,0 3,-1.346 3,-3 0,-1.302 -0.839,-2.401 -2,-2.816 v -1.851 c 0,-0.827 0.673,-1.5 1.5,-1.5 0.979,0 1.864,-0.407 2.5,-1.058 0.636,0.651 1.521,1.058 2.5,1.058 0.827,0 1.5,0.673 1.5,1.5 v 1.851 c -1.161,0.415 -2,1.514 -2,2.816 0,1.654 1.346,3 3,3 1.654,0 3,-1.346 3,-3 0,-1.302 -0.839,-2.401 -2,-2.816 z M 5,18 c -0.552,0 -1,-0.449 -1,-1 0,-0.551 0.448,-1 1,-1 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 z M 10,2 c 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 C 9.448,4 9,3.551 9,3 9,2.449 9.448,2 10,2 Z m 5,16 c -0.552,0 -1,-0.449 -1,-1 0,-0.551 0.448,-1 1,-1 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

+252
View File
@@ -0,0 +1,252 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="3"
inkscape:export-xdpi="3"
inkscape:export-filename="/home/vkbo/Code/novelWriter/Icons/novelWriter-32.png"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="mimetype-nwx.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient2515"
inkscape:collect="always">
<stop
id="stop2511"
offset="0"
style="stop-color:#f7f7f7;stop-opacity:1" />
<stop
id="stop2513"
offset="1"
style="stop-color:#e5e5e5;stop-opacity:1" />
</linearGradient>
<rect
id="rect852"
height="65.785888"
width="130.57787"
y="205.79962"
x="60.484347" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1474" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect852-7" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1544" />
<rect
id="rect1570"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1587"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter1444">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1434" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1436" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1438" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1440" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1442" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter1456">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1446" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1448" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1450" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1452" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1454" />
</filter>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
r="410"
fy="623.97186"
fx="605.42725"
cy="623.97186"
cx="605.42725"
id="radialGradient2517"
xlink:href="#linearGradient2515"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="505.1269"
inkscape:cy="568.88259"
inkscape:document-units="px"
inkscape:current-layer="layer3"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="2560"
inkscape:window-height="1344"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<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
style="display:inline"
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Sheet">
<path
id="rect1481"
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
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"
transform="scale(0.26458333)"
inkscape:label="Page"
clip-path="none" />
<path
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"
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"
id="path1490"
inkscape:label="Corner" />
<g
inkscape:label=".nwx"
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"
id="text850"
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
aria-label=".nwx">
<path
inkscape:label="dot"
id="path856"
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"
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" />
<path
inkscape:label="n"
id="path858"
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"
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" />
<path
inkscape:label="w"
id="path860"
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"
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" />
<path
inkscape:label="x"
id="path862"
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"
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" />
</g>
</g>
<g
inkscape:label="Logo"
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)"
id="g2509">
<path
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"
inkscape:label="n"
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)"
id="rect1406-1"
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)" />
<path
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"
inkscape:label="Arrow"
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)"
id="path897"
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)" />
<rect
inkscape:label="Highlight"
ry="3.6658845"
rx="3.3791983"
y="1.2969339"
x="-118.27303"
height="82.482399"
width="6.7583966"
id="rect914"
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+224
View File
@@ -0,0 +1,224 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="96.000008"
inkscape:export-xdpi="96.000008"
inkscape:export-filename="/home/vkbo/Code/novelWriter/Icons/novelWriter/Version 3/icons/1024x1024/novelwriter.png"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="icon.svg">
<defs
id="defs2">
<linearGradient
id="linearGradient1470"
inkscape:collect="always">
<stop
id="stop1466"
offset="0"
style="stop-color:#7a9cb8;stop-opacity:1" />
<stop
id="stop1468"
offset="1"
style="stop-color:#5978a6;stop-opacity:1" />
</linearGradient>
<rect
id="rect852"
height="65.785888"
width="130.57787"
y="205.79962"
x="60.484347" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1474" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect852-7" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1544" />
<rect
id="rect1570"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1587"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<filter
id="filter1444"
inkscape:label="Drop Shadow"
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"
inkscape:label="Drop Shadow"
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
gradientTransform="matrix(2.2584252,1.5089457,-0.74487144,1.1148423,-169.6027,-163.93817)"
gradientUnits="userSpaceOnUse"
r="130.96873"
fy="67.111206"
fx="111.19364"
cy="67.111206"
cx="111.19364"
id="radialGradient1474"
xlink:href="#linearGradient1470"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="190.20708"
inkscape:cy="572.85481"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="2560"
inkscape:window-height="1344"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<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
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="display:inline;fill:url(#radialGradient1474);fill-opacity:1;stroke:#517995;stroke-width:2.64583;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1467"
width="259.29166"
height="259.29166"
x="5.8208332"
y="5.8208332"
rx="21.166666"
ry="21.166666"
inkscape:label="Box" />
</g>
<g
style="display:inline"
inkscape:label="Logo"
id="layer2"
inkscape:groupmode="layer">
<path
transform="scale(0.26458333)"
d="m 312,200 c -44.32,0 -80,35.68 -80,80 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.68,80 80,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 762.83789 H 792 V 480.00195 a 279.99999,279.99999 0 0 0 -280,-280 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 C 363.30011,213.54621 339.29632,200 312,200 Z m 231.46094,563.22656 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"
inkscape:label="n"
style="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)"
id="rect1406" />
<path
transform="scale(0.26458333)"
d="m 632,762.83789 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 884.67773,775.08008 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 792 v -0.16406 z"
inkscape:label="Arrow"
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)"
id="path897" />
<rect
inkscape:label="Highlight"
ry="5.2916665"
rx="5.2916665"
y="67.997917"
x="69.849998"
height="119.0625"
width="10.583333"
id="rect914"
style="fill:#c32222;fill-opacity:1;stroke:none;stroke-width:2.64583;stroke-linejoin:round;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

+49
View File
@@ -0,0 +1,49 @@
<h1>Help!</h1>
<p><i>A brief guide to make the most out of the Build Novel Project tool.</i></p>
<h2>Novel Title Formats</h2>
<p>The format of the various title levels in the files under the Novel folder can be customised in
these settings. The actual title given in the headings of your files will for instance replace
all occurrences of the keyword <mark>%title%</mark>. Any static text will be left as-is in the
final title. An empty field means the title isn't written out at all.</p>
<p>The available formatting keywords are:</p>
<p><mark>%title%</mark> &ndash; This is replaced with the text you put in your headings in your
documents</p>
<p><mark>%chnum%</mark> &ndash; This is replaced with the chapter number of your chapter type
headings. These are generated automaticall starting from 1, but ignoring chapter headings in
files with "Unnumbered" layout.</p>
<p><mark>%chnumword%</mark> &ndash; This is replaced with the chapter number, but instead of an
arabic number, the word for it is used, e.g. One, Two, Fifteen, Twenty-Five, etc.</p>
<p><mark>%scnum%</mark> &ndash; This is replaced with the scene number. The number is reset to one
for each new chapter, so it is the scene number within the current chapter.</p>
<p><mark>%scabsnum%</mark> &ndash; This is replaced with the absolute scene number. That is, the
number is counted from the first scene in the novel, and not reset for each chapter.</p>
<p><mark>\\</mark> &ndash; Two backslashes are replaced by a line break.</p>
<p><b>Note:</b> The Scene and Section formats are treated slightly differently than the other title
formats. If the format is a constant text, that is, contains no <mark>%keyword%</mark> tags, it
will be treated as a separator instead. Scene and Section separators are centred, and for
scenes, not shown if placed directly after the chapter heading. For instance, it you want the
classic three asterisk <mark>* * *</mark> separator between scenes, just put that into the
scene format box, and nothing else.</p>
<h2>Build Overrides</h2>
<p><b>Novel Outline Mode:</b> This option will build an outline version of the novel rather than the
full thing. It overrides the title format settings without changing them. Each title will be
written out, and the synopsis text will appear instead of the body text of the files. Some of
the other options are still available in Outline Mode.</p>
<h2>Include Non-Text Elements</h2>
<p><b>Include Synopsis:</b> This will add the synopsis comment as the first paragraph after each
heading.</p>
<p><b>Include Comments:</b> This will include any comments as additional paragraphs in the text.</p>
<p><b>Include Keywords:</b> This will include any keywords and tags as clickable links after each
heading.</p>
<h2>Additional Options</h2>
<p><b>Include Novel Files:</b> This means all files that don't have a layout of type "Note" will be
included. This is the normal mode when exporting the novel itself without the notes.</p>
<p><b>Include Note Files:</b> This means all files with a layout of type "Note" <i>will</i> be
included. Titles in note files are always left as they appear.</p>
<p><b>Ignore Export Flag:</b> Each file in the project tree has an "Include when building project"
option set, which is indicated by a little check mark in the "Flags" column. Files without This
tick will normally be skipped during build, but can be included if this option is enabled.</p>
+641
View File
@@ -0,0 +1,641 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>GNU General Public License v3.0 - GNU Project - Free Software Foundation (FSF)</title>
<link rel="alternate" type="application/rdf+xml"
href="http://www.gnu.org/licenses/gpl-3.0.rdf" />
</head>
<body>
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
<p style="text-align: center;">Version 3, 29 June 2007</p>
<p>Copyright &copy; 2007 Free Software Foundation, Inc.
&lt;<a href="https://fsf.org/">https://fsf.org/</a>&gt;</p><p>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.</p>
<h3><a name="preamble"></a>Preamble</h3>
<p>The GNU General Public License is a free, copyleft license for
software and other kinds of works.</p>
<p>The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.</p>
<p>When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.</p>
<p>To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.</p>
<p>For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.</p>
<p>Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.</p>
<p>For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.</p>
<p>Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.</p>
<p>Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.</p>
<p>The precise terms and conditions for copying, distribution and
modification follow.</p>
<h3><a name="terms"></a>TERMS AND CONDITIONS</h3>
<h4><a name="section0"></a>0. Definitions.</h4>
<p>&ldquo;This License&rdquo; refers to version 3 of the GNU General Public License.</p>
<p>&ldquo;Copyright&rdquo; also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.</p>
<p>&ldquo;The Program&rdquo; refers to any copyrightable work licensed under this
License. Each licensee is addressed as &ldquo;you&rdquo;. &ldquo;Licensees&rdquo; and
&ldquo;recipients&rdquo; may be individuals or organizations.</p>
<p>To &ldquo;modify&rdquo; a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a &ldquo;modified version&rdquo; of the
earlier work or a work &ldquo;based on&rdquo; the earlier work.</p>
<p>A &ldquo;covered work&rdquo; means either the unmodified Program or a work based
on the Program.</p>
<p>To &ldquo;propagate&rdquo; a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.</p>
<p>To &ldquo;convey&rdquo; a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.</p>
<p>An interactive user interface displays &ldquo;Appropriate Legal Notices&rdquo;
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.</p>
<h4><a name="section1"></a>1. Source Code.</h4>
<p>The &ldquo;source code&rdquo; for a work means the preferred form of the work
for making modifications to it. &ldquo;Object code&rdquo; means any non-source
form of a work.</p>
<p>A &ldquo;Standard Interface&rdquo; means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.</p>
<p>The &ldquo;System Libraries&rdquo; of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
&ldquo;Major Component&rdquo;, in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.</p>
<p>The &ldquo;Corresponding Source&rdquo; for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.</p>
<p>The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.</p>
<p>The Corresponding Source for a work in source code form is that
same work.</p>
<h4><a name="section2"></a>2. Basic Permissions.</h4>
<p>All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.</p>
<p>You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.</p>
<p>Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.</p>
<h4><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h4>
<p>No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.</p>
<p>When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.</p>
<h4><a name="section4"></a>4. Conveying Verbatim Copies.</h4>
<p>You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.</p>
<p>You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.</p>
<h4><a name="section5"></a>5. Conveying Modified Source Versions.</h4>
<p>You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:</p>
<ul>
<li>a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.</li>
<li>b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
&ldquo;keep intact all notices&rdquo;.</li>
<li>c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.</li>
<li>d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.</li>
</ul>
<p>A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
&ldquo;aggregate&rdquo; if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.</p>
<h4><a name="section6"></a>6. Conveying Non-Source Forms.</h4>
<p>You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:</p>
<ul>
<li>a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.</li>
<li>b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.</li>
<li>c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.</li>
<li>d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.</li>
<li>e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.</li>
</ul>
<p>A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.</p>
<p>A &ldquo;User Product&rdquo; is either (1) a &ldquo;consumer product&rdquo;, which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, &ldquo;normally used&rdquo; refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.</p>
<p>&ldquo;Installation Information&rdquo; for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.</p>
<p>If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).</p>
<p>The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.</p>
<p>Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.</p>
<h4><a name="section7"></a>7. Additional Terms.</h4>
<p>&ldquo;Additional permissions&rdquo; are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.</p>
<p>When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.</p>
<p>Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:</p>
<ul>
<li>a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or</li>
<li>b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or</li>
<li>c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or</li>
<li>d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or</li>
<li>e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or</li>
<li>f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.</li>
</ul>
<p>All other non-permissive additional terms are considered &ldquo;further
restrictions&rdquo; within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.</p>
<p>If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.</p>
<p>Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.</p>
<h4><a name="section8"></a>8. Termination.</h4>
<p>You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).</p>
<p>However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.</p>
<p>Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.</p>
<p>Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.</p>
<h4><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h4>
<p>You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.</p>
<h4><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h4>
<p>Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.</p>
<p>An &ldquo;entity transaction&rdquo; is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.</p>
<p>You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.</p>
<h4><a name="section11"></a>11. Patents.</h4>
<p>A &ldquo;contributor&rdquo; is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's &ldquo;contributor version&rdquo;.</p>
<p>A contributor's &ldquo;essential patent claims&rdquo; are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, &ldquo;control&rdquo; includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.</p>
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.</p>
<p>In the following three paragraphs, a &ldquo;patent license&rdquo; is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To &ldquo;grant&rdquo; such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.</p>
<p>If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. &ldquo;Knowingly relying&rdquo; means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.</p>
<p>If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.</p>
<p>A patent license is &ldquo;discriminatory&rdquo; if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.</p>
<p>Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.</p>
<h4><a name="section12"></a>12. No Surrender of Others' Freedom.</h4>
<p>If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.</p>
<h4><a name="section13"></a>13. Use with the GNU Affero General Public License.</h4>
<p>Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.</p>
<h4><a name="section14"></a>14. Revised Versions of this License.</h4>
<p>The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.</p>
<p>Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License &ldquo;or any later version&rdquo; applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.</p>
<p>If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.</p>
<p>Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.</p>
<h4><a name="section15"></a>15. Disclaimer of Warranty.</h4>
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM &ldquo;AS IS&rdquo; WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p>
<h4><a name="section16"></a>16. Limitation of Liability.</h4>
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.</p>
<h4><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h4>
<p>If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.</p>
<p>END OF TERMS AND CONDITIONS</p>
</body></html>
+6 -4
View File
@@ -1,8 +1,10 @@
[Main]
name = Default Dark Theme
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
name = Default Dark Theme
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/
[Palette]
window = 54, 54, 54
+6 -1
View File
@@ -1,5 +1,10 @@
[Main]
name = Default Dark
name = Default 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
+6 -1
View File
@@ -1,5 +1,10 @@
[Main]
name = Default Light
name = Default 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
+6 -4
View File
@@ -19,10 +19,12 @@
##
[Main]
name = Light Owl
author = Veronica Berglyd Olsen
credit = Sarah Drasner
url = https://github.com/sdras/night-owl-vscode-theme
name = Light Owl
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
[Syntax]
background = 251, 251, 251
+6 -4
View File
@@ -19,10 +19,12 @@
##
[Main]
name = Night Owl
author = Veronica Berglyd Olsen
credit = Sarah Drasner
url = https://github.com/sdras/night-owl-vscode-theme
name = Night Owl
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
[Syntax]
background = 1, 22, 39
+6 -4
View File
@@ -19,10 +19,12 @@
##
[Main]
name = Tomorrow
author = Veronica Berglyd Olsen
credit = Chris Kempson
url = https://github.com/chriskempson/tomorrow-theme
name = Tomorrow
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 255, 255, 255
+6 -4
View File
@@ -19,10 +19,12 @@
##
[Main]
name = Tomorrow Night
author = Veronica Berglyd Olsen
credit = Chris Kempson
url = https://github.com/chriskempson/tomorrow-theme
name = Tomorrow Night
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 29, 31, 33
@@ -19,10 +19,12 @@
##
[Main]
name = Tomorrow Night Blue
author = Veronica Berglyd Olsen
credit = Chris Kempson
url = https://github.com/chriskempson/tomorrow-theme
name = Tomorrow Night Blue
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 0, 36, 81
@@ -19,10 +19,12 @@
##
[Main]
name = Tomorrow Night Bright
author = Veronica Berglyd Olsen
credit = Chris Kempson
url = https://github.com/chriskempson/tomorrow-theme
name = Tomorrow Night Bright
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 0, 0, 0
@@ -19,10 +19,12 @@
##
[Main]
name = Tomorrow Night Eighties
author = Veronica Berglyd Olsen
credit = Chris Kempson
url = https://github.com/chriskempson/tomorrow-theme
name = Tomorrow Night Eighties
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 45, 45, 45
-8
View File
@@ -172,11 +172,3 @@ def splitVersionNumber(vString):
vInt = vMajor*10000 + vMinor*100 + vPatch
return [vMajor, vMinor, vPatch, vInt]
def packageRefURL(packName):
from nw.constants import nwDependencies
if packName in nwDependencies.PACKS.keys():
return "<a href=\"%s\">%s</a>" % (
nwDependencies.PACKS[packName]["site"], packName
)
return packName
+2 -1
View File
@@ -86,6 +86,7 @@ class Config:
self.guiSyntax = "default_light"
self.guiIcons = "typicons_grey_light"
self.guiDark = False
self.guiLang = "en" # Hardcoded for now
## Sizes
self.winGeometry = [1100, 650]
@@ -232,7 +233,7 @@ class Config:
self.graphPath = path.join(self.assetPath,"graphics")
self.dictPath = path.join(self.assetPath,"dict")
self.iconPath = path.join(self.assetPath,"icons")
self.appIcon = path.join(self.iconPath, nwFiles.APP_ICON)
self.appIcon = path.join(self.iconPath, "novelwriter.svg")
logger.verbose("App path: %s" % self.appPath)
logger.verbose("Home path: %s" % self.homePath)
+1 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import (
nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode
nwConst, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline
@@ -14,7 +14,6 @@ __all__ = [
"nwFiles",
"nwKeyWords",
"nwLabels",
"nwDependencies",
"nwQuotes",
"nwUnicode",
"nwAlert",
-29
View File
@@ -36,7 +36,6 @@ class nwConst():
class nwFiles():
APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
@@ -154,34 +153,6 @@ class nwLabels():
# END Class nwLabels
class nwDependencies():
"""Python package dependencies and their reference links.
"""
PACKS = {
"pyqt5" : {
"site" : "",
"docs" : "",
},
"lxml" : {
"site" : "",
"docs" : "",
},
"pyenchant" : {
"site" : "",
"docs" : "",
},
"latexcodec" : {
"site" : "https://pypi.org/project/latexcodec/",
"docs" : "https://latexcodec.readthedocs.io/en/latest/",
},
"pypandoc" : {
"site" : "https://pypi.org/project/pypandoc/",
"docs" : "https://pypi.org/project/pypandoc/",
},
}
# END Class nwDependencies
class nwQuotes():
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
-27
View File
@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.tokenizer import Tokenizer
from nw.convert.file.concat import ConcatFile
from nw.convert.file.html import HtmlFile
from nw.convert.file.latex import LaTeXFile
from nw.convert.file.markdown import MarkdownFile
from nw.convert.file.text import TextFile
from nw.convert.text.tohtml import ToHtml
from nw.convert.text.tolatex import ToLaTeX
from nw.convert.text.tomarkdown import ToMarkdown
from nw.convert.text.totext import ToText
__all__ = [
"Tokenizer",
"ConcatFile",
"HtmlFile",
"LaTeXFile",
"MarkdownFile",
"TextFile",
"ToHtml",
"ToLaTeX",
"ToMarkdown",
"ToText",
]
-15
View File
@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.file.concat import ConcatFile
from nw.convert.file.html import HtmlFile
from nw.convert.file.latex import LaTeXFile
from nw.convert.file.markdown import MarkdownFile
from nw.convert.file.text import TextFile
__all__ = [
"ConcatFile",
"HtmlFile",
"LaTeXFile",
"MarkdownFile",
"TextFile",
]
-80
View File
@@ -1,80 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Concatenated File
novelWriter Concatenated File
=================================
Concatenate the standard novelWriter files to a single file
File History:
Created: 2019-10-26 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from nw.convert.file.text import TextFile
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class ConcatFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = Tokenizer(self.theProject, self.theParent)
return
def addText(self, tHandle):
logger.verbose("Parsing content of item '%s'" % tHandle)
if not self.checkInclude(tHandle):
return False
self.theConv.setText(tHandle)
theResult = self.theConv.theText
if theResult is not None and self.outFile is not None:
self.outFile.write(theResult.rstrip())
self.outFile.write("\n\n")
return True
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.close()
return True
# END Class ConcatFile
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML File
novelWriter HTML File
=========================
Writes the project to a html file
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tohtml import ToHtml
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class HtmlFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToHtml(self.theProject, self.theParent)
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("<!DOCTYPE html>\n")
self.outFile.write("<html>\n")
self.outFile.write("<head>\n")
self.outFile.write(" <meta charset='utf-8'>\n")
self.outFile.write(" <style>\n")
self.outFile.write(" #page {\n")
self.outFile.write(" margin: 40px auto;\n")
self.outFile.write(" max-width: 769px;\n")
self.outFile.write(" }\n")
self.outFile.write(" .comment {\n")
self.outFile.write(" background-color: #fbfabd;\n")
self.outFile.write(" border: 1px solid #b4b000;\n")
self.outFile.write(" margin: 10px 20px;\n")
self.outFile.write(" padding: 6px;\n")
self.outFile.write(" }\n")
self.outFile.write(" </style>\n")
self.outFile.write("</head>\n")
self.outFile.write("<body>\n")
self.outFile.write("<article id='page'>\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.write("</article>\n")
self.outFile.write("</body>\n")
self.outFile.write("</html>\n")
self.outFile.close()
return True
# END Class HtmlFile
-69
View File
@@ -1,69 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter LaTeX File
novelWriter LaTeX File
==========================
Writes the project to a LaTeX file
File History:
Created: 2019-10-24 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tolatex import ToLaTeX
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class LaTeXFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToLaTeX(self.theProject, self.theParent)
self.texCodecFail = False
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("\\documentclass[12pt]{report}\n")
self.outFile.write("\\usepackage[utf8]{inputenc}\n")
self.outFile.write("\\usepackage[T1]{fontenc}\n")
self.outFile.write("\n")
self.outFile.write("\\begin{document}\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.write("\\end{document}\n")
self.outFile.close()
self.texCodecFail = self.theConv.texCodecFail
return True
# END Class LaTeXFile
-61
View File
@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Markdown File
novelWriter Markdown File
=============================
Writes the project to a markdown file
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from nw.convert.file.text import TextFile
from nw.convert.text.tomarkdown import ToMarkdown
from nw.constants import nwAlert
logger = logging.getLogger(__name__)
class MarkdownFile(TextFile):
def __init__(self, theProject, theParent):
TextFile.__init__(self, theProject, theParent)
self.theConv = ToMarkdown(self.theProject, self.theParent)
return
##
# Internal Functions
##
def _doOpenFile(self, filePath):
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
if self.outFile is not None:
self.outFile.close()
return True
# END Class MarkdownFile
-210
View File
@@ -1,210 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Text File
novelWriter Text File
=========================
Writes the project to a plain text file
File History:
Created: 2019-10-18 [0.2.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from PyQt5.QtWidgets import QMessageBox
from nw.convert.text.totext import ToText
from nw.constants import nwAlert, nwItemType, nwItemLayout, nwItemClass
logger = logging.getLogger(__name__)
class TextFile():
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.outFile = None
self.fileName = ""
self.theText = ""
self.expNovel = True
self.expNotes = False
self.theConv = ToText(self.theProject, self.theParent)
self.makeAlert = self.theParent.makeAlert
self.setComments(False)
self.setKeywords(False)
self.setWordWrap(80)
return
##
# Setters
##
def setExportNovel(self, doNovel):
self.expNovel = doNovel
return
def setExportNotes(self, doNotes):
self.expNotes = doNotes
return
def setComments(self, doComments):
self.theConv.setComments(doComments)
return
def setKeywords(self, doKeywords):
self.theConv.setKeywords(doKeywords)
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.theConv.setWordWrap(wordWrap)
else:
self.theConv.setWordWrap(0)
return
def setTitleFormat(self, fmtTitle):
self.theConv.setTitleFormat(fmtTitle)
return
def setChapterFormat(self, fmtChapter):
self.theConv.setChapterFormat(fmtChapter)
return
def setUnNumberedFormat(self, fmtUnNum):
self.theConv.setUnNumberedFormat(fmtUnNum)
return
def setSceneFormat(self, fmtScene, hideScene):
self.theConv.setSceneFormat(fmtScene, hideScene)
return
def setSectionFormat(self, fmtSection, hideSection):
self.theConv.setSectionFormat(fmtSection, hideSection)
return
##
# Core Methods
##
def openFile(self, filePath):
self.fileName = path.basename(filePath)
if path.isfile(filePath) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Overwrite", (
"File '%s' already exists.<br>Do you want to overwrite it?" % self.fileName
))
if msgRes != QMessageBox.Yes:
return False
self._doOpenFile(filePath)
if self.outFile is None:
return False
return True
def closeFile(self):
self._doCloseFile()
return True
def addText(self, tHandle):
logger.verbose("Parsing content of item '%s'" % tHandle)
if not self.checkInclude(tHandle):
return False
self.theConv.setText(tHandle)
self.theConv.doAutoReplace()
self.theConv.tokenizeText()
self.theConv.doHeaders()
self.theConv.doConvert()
self.theConv.doPostProcessing()
if self.theConv.theResult is not None and self.outFile is not None:
self.outFile.write(self.theConv.theResult)
return True
def checkInclude(self, tHandle):
"""This function checks whether a file should be included in the
export or not. For standard note and novel files, this is
controlled by the options selected by the user. For other files
classified as non-exportable, a few checks must be made, and the
following are not:
* Items that are not actual files.
* Items that have been orphaned which are tagged as NO_LAYOUT
and NO_CLASS.
* Items that appear in the TRASH folder
"""
theItem = self.theProject.projTree[tHandle]
isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot()
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
if isNone:
return False
if isNote and not self.expNotes:
return False
if isNovel and not self.expNovel:
return False
return True
##
# Internal Functions
##
def _doOpenFile(self, filePath):
"""This function does the actual opening of the file, and can be
overloaded by a subclass that uses a different file format that
requires a different approach.
"""
try:
self.outFile = open(filePath,mode="wt+",encoding="utf8")
self.outFile.write("\n\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
"""This function closes the file, and is meant to be overloaded
by the subclass for other file formats.
"""
if self.outFile is not None:
self.outFile.close()
return True
# END Class OutFile
-13
View File
@@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from nw.convert.text.tohtml import ToHtml
from nw.convert.text.tolatex import ToLaTeX
from nw.convert.text.tomarkdown import ToMarkdown
from nw.convert.text.totext import ToText
__all__ = [
"ToHtml",
"ToLaTeX",
"ToMarkdown",
"ToText",
]
-176
View File
@@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML Text Converter
novelWriter HTML Text Converter
===================================
Extends the Tokenizer class to write HTML
File History:
Created: 2019-05-07 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode, nwLabels
logger = logging.getLogger(__name__)
class ToHtml(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.forPreview = False
return
def setPreview(self, forPreview, doComments):
"""If we're using this class to generate markdown preview, we
need to make a few changes to formatting, which is selected by
this flag.
"""
self.forPreview = forPreview
if forPreview:
self.doKeywords = True
self.doComments = doComments
return
def doAutoReplace(self):
Tokenizer.doAutoReplace(self)
if self.forPreview:
tabFmt = "&nbsp;"*8
else:
tabFmt = "&emsp;"
repDict = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"\t" : tabFmt,
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doConvert(self):
htmlTags = {
self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>",
self.FMT_I_B : "<em>",
self.FMT_I_E : "</em>",
self.FMT_U_B : "<u>",
self.FMT_U_E : "</u>",
}
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
aStyle = []
if tAlign == self.A_CENTRE:
aStyle.append("text-align: center;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
else:
hStyle = ""
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "".join(thisPar)
self.theResult += "<p%s>%s</p>\n" % (hStyle,tTemp.rstrip())
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += "<h1%s>%s</h1>\n" % (hStyle,tText)
elif tType == self.T_HEAD2:
self.theResult += "<h2%s>%s</h2>\n" % (hStyle,tText)
elif tType == self.T_HEAD3:
self.theResult += "<h3%s>%s</h3>\n" % (hStyle,tText)
elif tType == self.T_HEAD4:
self.theResult += "<h4%s>%s</h4>\n" % (hStyle,tText)
elif tType == self.T_SEP:
self.theResult += "<p%s>%s</p>\n" % (hStyle,tText)
elif tType == self.T_SKIP:
self.theResult += "<p>&nbsp;</p>\n"
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>")
else:
thisPar.append(tTemp.rstrip()+" ")
elif tType == self.T_COMMENT and self.doComments:
self.theResult += self._formatComments(tText)
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += self._formatTags(tText)
return
##
# Internal Functions
##
def _formatTags(self, tText):
if not self.forPreview:
return "<pre>@%s</pre>\n" % tText
tText = "@"+tText
isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
return "<div>%s</div>" % retText
def _formatComments(self, tText):
if not self.forPreview:
return "<div class='comment'>%s</div>\n" % tText
return "<p class='comment'>%s</p>\n" % tText
# END Class ToHtml
-155
View File
@@ -1,155 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter LaTeX Converter
novelWriter LaTeX Converter
===============================
Extends the Tokenizer class to write LaTeX
File History:
Created: 2019-10-24 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import codecs
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
class ToLaTeX(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.texCodecFail = False
return
def doPostProcessing(self):
"""The latexcodec misses dashes and non-breaking spaces, so we
do those here.
"""
repDict = {
nwUnicode.U_ENDASH : "--",
nwUnicode.U_EMDASH : "---",
nwUnicode.U_NBSP : "~",
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theResult = xRep.sub(lambda x: repDict[x.group(0)], self.theResult)
return
def doConvert(self):
texTags = {
self.FMT_B_B : r"\textbf{",
self.FMT_B_E : r"}",
self.FMT_I_B : r"\textit{",
self.FMT_I_E : r"}",
self.FMT_U_B : r"\underline{",
self.FMT_U_E : r"}",
}
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
begText = ""
endText = "\n"
if tAlign == self.A_CENTRE:
begText = "\\begin{center}\n"
endText = "\\end{center}\n\n"
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = "%% %s" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+texTags[xFmt]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
self.theResult += begText
for tTemp in thisPar:
self.theResult += "%s\n" % tTemp
self.theResult += endText
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += begText
self.theResult += "{\\Huge %s}\n" % self._escapeUnicode(tText)
self.theResult += endText
elif tType == self.T_HEAD2:
self.theResult += "\\chapter*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_HEAD3:
self.theResult += "\\section*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_HEAD4:
self.theResult += "\\subsection*{%s}\n\n" % self._escapeUnicode(tText)
elif tType == self.T_SEP:
self.theResult += begText
self.theResult += "%s\n" % self._escapeUnicode(tText)
self.theResult += endText
elif tType == self.T_SKIP:
self.theResult += "\\bigskip\n"
self.theResult += "\\bigskip\n\n"
elif tType == self.T_TEXT:
if tText.endswith(" "):
thisPar.append(self._escapeUnicode(tText.rstrip())+"\\newline")
else:
thisPar.append(self._escapeUnicode(tText.rstrip()))
elif tType == self.T_PBREAK:
self.theResult += "\\newpage\n\n"
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%% @%s\n\n" % tText
return
def _escapeUnicode(self, theText):
try:
import latexcodec
return codecs.encode(theText, "ulatex+utf8")
except:
self.texCodecFail = True
return theText
# END Class ToLaTeX
-136
View File
@@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Markdown Text Converter
novelWriter Markdown Text Converter
=======================================
Extends the Tokenizer class to write Markdown
File History:
Created: 2019-10-19 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import textwrap
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
logger = logging.getLogger(__name__)
class ToMarkdown(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
return
def doConvert(self):
mdTags = {
self.FMT_B_B : "**",
self.FMT_B_E : "**",
self.FMT_I_B : "_",
self.FMT_I_E : "_",
self.FMT_U_B : "__",
self.FMT_U_E : "__",
}
if self.wordWrap > 0:
tWrap = textwrap.TextWrapper(
width = self.wordWrap,
initial_indent = "",
subsequent_indent = "",
expand_tabs = True,
replace_whitespace = True,
fix_sentence_endings = False,
break_long_words = True,
drop_whitespace = True,
break_on_hyphens = True,
tabsize = 8,
max_lines = None
)
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = " %s" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+mdTags[xFmt]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# The text can now be word wrapped, if we have requested
# this and it's needed.
if self.wordWrap > 0 and tLen > self.wordWrap:
if tType == self.T_COMMENT:
tText = textwrap.fill(
tText.strip(),initial_indent=" ",subsequent_indent=" "
)
else:
tText = tWrap.fill(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line,
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "\n".join(thisPar)
self.theResult += "%s\n\n" % tTemp.rstrip()
thisPar = []
elif tType == self.T_HEAD1:
self.theResult += "# %s\n\n" % tText
elif tType == self.T_HEAD2:
self.theResult += "## %s\n\n" % tText
elif tType == self.T_HEAD3:
self.theResult += "### %s\n\n" % tText
elif tType == self.T_HEAD4:
self.theResult += "#### %s\n\n" % tText
elif tType == self.T_SEP:
self.theResult += "%s\n\n" % tText
elif tType == self.T_SKIP:
self.theResult += "\n\n\n"
elif tType == self.T_TEXT:
thisPar.append(tText)
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%s\n\n" % tText
return
# END Class ToMarkdown
-154
View File
@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Plain Text Converter
novelWriter Plain Text Converter
====================================
Extends the Tokenizer class to convert to plain text
File History:
Created: 2019-10-26 [0.3.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import textwrap
import logging
import re
import nw
from nw.convert.tokenizer import Tokenizer
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
class ToText(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
return
def doAutoReplace(self):
Tokenizer.doAutoReplace(self)
repDict = {
"\t" : " ",
nwUnicode.U_NBSP : " ",
}
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doConvert(self):
"""Converts the tokenized text into plain text.
"""
if self.wordWrap > 0:
tWrap = textwrap.TextWrapper(
width = self.wordWrap,
initial_indent = "",
subsequent_indent = "",
expand_tabs = True,
replace_whitespace = True,
fix_sentence_endings = False,
break_long_words = True,
drop_whitespace = True,
break_on_hyphens = True,
tabsize = 8,
max_lines = None
)
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
# First check if we have a comment or plain text, as they
# need some extra replacing before we proceed to wrapping
# and final formatting.
if tType == self.T_COMMENT:
tText = "[%s]" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# The text can now be word wrapped, if we have requested
# this and it's needed.
if tAlign == self.A_CENTRE:
if self.wordWrap > 0:
if tLen > self.wordWrap:
aText = tWrap.wrap(tText)
for n in range(len(aText)):
aText[n] = self._centreText(aText[n],self.wordWrap)
tText = "\n".join(aText)
else:
tText = self._centreText(tText,self.wordWrap)
else:
if self.wordWrap > 0 and tLen > self.wordWrap:
tText = tWrap.fill(tText)
# Then the text can receive final formatting before we
# append it to the results. We also store text lines in a
# buffer and merge them only when we find an empty line,
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "\n".join(thisPar)
self.theResult += "%s\n\n" % tTemp.rstrip()
thisPar = []
elif tType == self.T_HEAD1:
uLine = "="*min(tLen,self.wordWrap)
if tAlign == self.A_CENTRE:
uLine = self._centreText(uLine,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD2:
uLine = "~"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD3:
uLine = "-"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD4:
self.theResult += "%s\n\n" % tText
elif tType == self.T_SEP:
if self.wordWrap > 0 and tLen < self.wordWrap:
tText = self._centreText(tText,self.wordWrap)
self.theResult += "%s\n\n" % tText
elif tType == self.T_SKIP:
self.theResult += "\n\n\n"
elif tType == self.T_TEXT:
thisPar.append(tText)
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_KEYWORD and self.doKeywords:
self.theResult += "%s\n\n" % tText
return
# END Class ToText
-345
View File
@@ -1,345 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Text Tokenizer
novelWriter Text Tokenizer
==============================
Splits a piece of nW markdown text into its elements
File History:
Created: 2019-05-05 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord
from nw.constants import nwItemLayout
logger = logging.getLogger(__name__)
class Tokenizer():
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_U_B = 5 # Begin underline
FMT_U_E = 6 # End underline
T_EMPTY = 1 # Empty line (new paragraph)
T_COMMENT = 2 # Comment line
T_KEYWORD = 3 # Command line
T_HEAD1 = 4 # Header 1 (title)
T_HEAD2 = 5 # Header 2 (chapter)
T_HEAD3 = 6 # Header 3 (scene)
T_HEAD4 = 7 # Header 4
T_TEXT = 8 # Text line
T_SEP = 9 # Scene separator
T_SKIP = 10 # Paragraph break
T_PBREAK = 11 # Page break
A_LEFT = 1 # Left aligned
A_RIGHT = 2 # Right aligned
A_CENTRE = 3 # Centred
A_JUSTIFY = 4 # Justified
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theText = None
self.theHandle = None
self.theItem = None
self.theTokens = None
self.theResult = None
self.wordWrap = 0
self.doComments = False
self.doKeywords = False
self.fmtTitle = "%title%"
self.fmtChapter = "%title%"
self.fmtUnNum = "%title%"
self.fmtScene = "%title%"
self.fmtSection = "%title%"
self.hideScene = False
self.hideSection = False
self.numChapter = 0
self.firstScene = False
return
##
# Setters
##
def setComments(self, doComments):
self.doComments = doComments
return
def setKeywords(self, doKeywords):
self.doKeywords = doKeywords
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.wordWrap = wordWrap
else:
self.wordWrap = 0
return
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum
return
def setSceneFormat(self, fmtScene, hideScene):
self.fmtScene = fmtScene
self.hideScene = hideScene
return
def setSectionFormat(self, fmtSection, hideSection):
self.fmtSection = fmtSection
self.hideSection = hideSection
return
##
# Class Methods
##
def setText(self, theHandle, theText=None):
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if theText is not None:
# If the text is set, just use that
self.theText = theText
else:
# Otherwise, load it from file
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
return
def doAutoReplace(self):
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
return
def tokenizeText(self):
"""Scan the text for either lines starting with specific
characters that indicate headers, comments, commands etc, or
just contains plain text. in the case of plain text, apply the
same RegExes that the syntax highlighter uses and save the
locations of these formatting tags into the token array.
"""
# RegExes for adding formatting tags within text lines
# Keep in sync with the DocHighlighter class
rxFormats = [(
QRegularExpression(r"(?<![\w|\\])([\*]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_B_B, None, self.FMT_B_E]
),(
QRegularExpression(r"(?<![\w|_|\\])([_])(?!\s|\1)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_I_B, None, self.FMT_I_E]
),(
QRegularExpression(r"(?<![\w|\\])([_]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_U_B, None, self.FMT_U_E]
)]
self.theTokens = []
for aLine in self.theText.splitlines():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
elif aLine[0] == "%":
self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT))
elif aLine[0] == "@":
self.theTokens.append((self.T_KEYWORD,aLine[1:].strip(),None,self.A_LEFT))
elif aLine[:2] == "# ":
self.theTokens.append((self.T_HEAD1,aLine[2:].strip(),None,self.A_LEFT))
elif aLine[:3] == "## ":
self.theTokens.append((self.T_HEAD2,aLine[3:].strip(),None,self.A_LEFT))
elif aLine[:4] == "### ":
self.theTokens.append((self.T_HEAD3,aLine[4:].strip(),None,self.A_LEFT))
elif aLine[:5] == "#### ":
self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT))
else:
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
for theRX, theKeys in rxFormats:
rxThis = theRX.globalMatch(aLine, 0)
while rxThis.hasNext():
rxMatch = rxThis.next()
for n in range(1,len(theKeys)):
if theKeys[n] is not None:
xPos = rxMatch.capturedStart(n)
xLen = rxMatch.capturedLength(n)
fmtPos.append([xPos,xLen,theKeys[n]])
# Save the line as is, but append the array of formatting locations
# sorted by position
fmtPos = sorted(fmtPos,key=itemgetter(0))
self.theTokens.append((self.T_TEXT,aLine,fmtPos,self.A_LEFT))
# Always add an empty line at the end
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
return
def doHeaders(self):
isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
isBook = self.theItem.itemLayout == nwItemLayout.BOOK
isPage = self.theItem.itemLayout == nwItemLayout.PAGE
isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
isScene = self.theItem.itemLayout == nwItemLayout.SCENE
isNote = self.theItem.itemLayout == nwItemLayout.NOTE
# No special header formatting for notes and no-layout files
if isNone: return
if isNote: return
# For novel files, we need to handle chapter numbering and scene
# breaks
if isBook or isUnNum or isChap or isScene:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
if tType == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD2:
if not isUnNum:
self.numChapter += 1
tText = self._doFormatChapter(tText,isUnNum)
self.theTokens[n] = (tType,tText,None,self.A_LEFT)
self.firstScene = True
elif tType == self.T_HEAD3:
tTemp = self._doFormatScene(tText)
if tTemp == "" and self.hideScene:
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
else:
self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
else:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
else:
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
self.firstScene = False
elif tType == self.T_HEAD4:
tTemp = self._doFormatSection(tText)
if tTemp == "" and self.hideSection:
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (self.T_SKIP,"",None,self.A_LEFT)
elif tTemp == self.fmtSection:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
else:
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
# For title page and partitions, we need to centre all text
# and for some formats, we need a page break
if isTitle or isPart:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE)
self.theTokens.append((self.T_PBREAK,"",None,self.A_LEFT))
return
##
# Internal Functions
##
def _doFormatTitle(self, theText):
theTitle = self.fmtTitle
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatChapter(self, theText, noNum):
if noNum:
theTitle = self.fmtUnNum
theTitle = theTitle.replace("%title%", theText)
else:
theTitle = self.fmtChapter
theTitle = theTitle.replace("%title%", theText)
theTitle = theTitle.replace("%num%", str(self.numChapter))
theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en"))
return theTitle
def _doFormatScene(self, theText):
theTitle = self.fmtScene
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatSection(self, theText):
theTitle = self.fmtSection
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _centreText(self, theText, theWidth):
tLen = len(theText)
if tLen < theWidth:
return " "*int((theWidth-tLen)/2) + theText
return theText
# END Class Tokenizer
+4
View File
@@ -6,6 +6,8 @@ from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck
from nw.core.spellcheck import NWSpellEnchant
from nw.core.spellcheck import NWSpellSimple
from nw.core.tokenizer import Tokenizer
from nw.core.tohtml import ToHtml
from nw.core.tools import countWords
from nw.core.tools import projectMaintenance
from nw.core.tools import numberToWord
@@ -17,6 +19,8 @@ __all__ = [
"NWSpellCheck",
"NWSpellEnchant",
"NWSpellSimple",
"Tokenizer",
"ToHtml",
"countWords",
"projectMaintenance",
"numberToWord",
+38 -45
View File
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class NWIndex():
VALID_KEYS = [
VALID_KEYS = set([
nwKeyWords.TAG_KEY,
nwKeyWords.PLOT_KEY,
nwKeyWords.POV_KEY,
@@ -51,7 +51,7 @@ class NWIndex():
nwKeyWords.OBJECT_KEY,
nwKeyWords.ENTITY_KEY,
nwKeyWords.CUSTOM_KEY
]
])
TAG_CLASS = {
nwKeyWords.CHAR_KEY : nwItemClass.CHARACTER,
nwKeyWords.POV_KEY : nwItemClass.CHARACTER,
@@ -107,7 +107,6 @@ class NWIndex():
def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle.
"""
delTags = []
for tTag in self.tagIndex:
if self.tagIndex[tTag][1] == tHandle:
@@ -130,7 +129,6 @@ class NWIndex():
def loadIndex(self):
"""Load index from last session from the project meta folder.
"""
theData = {}
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
@@ -169,7 +167,6 @@ class NWIndex():
"""Save the current index as a json file in the project meta
data folder.
"""
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
logger.debug("Saving index file")
@@ -179,7 +176,7 @@ class NWIndex():
nIndent = None
try:
with open(indexFile,mode="w+",encoding="utf8") as outFile:
with open(indexFile, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps({
"tagIndex" : self.tagIndex,
"refIndex" : self.refIndex,
@@ -198,7 +195,6 @@ class NWIndex():
"""Check that the entries in the index are valid and contain the
elements it should.
"""
self.indexBroken = False
try:
@@ -265,7 +261,7 @@ class NWIndex():
logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index
# Also add an entry for T0 in case the file has no title
# Also add a dummy entry for T0 in case the file has no title
self.refIndex[tHandle] = {}
self.refIndex[tHandle]["T0"] = {
"tags" : [],
@@ -297,16 +293,16 @@ class NWIndex():
continue
if aLine.startswith(r"#"):
isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
isTitle = self._indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
if isTitle and nLine > 0:
if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1])
self.indexWordCounts(tHandle, isNovel, lastText, nTitle)
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
nTitle = nLine
elif aLine.startswith(r"@"):
self.indexNoteRef(tHandle, aLine, nLine, nTitle)
self.indexTag(tHandle, aLine, nLine, itemClass)
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
self._indexTag(tHandle, aLine, nLine, itemClass)
elif aLine.startswith(r"%"):
if nTitle > 0:
@@ -315,12 +311,12 @@ class NWIndex():
cLen = len(toCheck)
cOff = tLen - cLen
if toCheck.startswith("synopsis:"):
self.indexSynopsis(tHandle, isNovel, aLine[cOff+9:].strip(), nTitle)
self._indexSynopsis(tHandle, isNovel, aLine[cOff+9:].strip(), nTitle)
# Count words for remaining text after last heading
if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1])
self.indexWordCounts(tHandle, isNovel, lastText, nTitle)
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
# Run word counter for whole text
cC, wC, pC = countWords(theText)
@@ -336,11 +332,14 @@ class NWIndex():
return True
def indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout):
"""Save information about the title and its location in the
file.
"""
##
# Internal Indexers
##
def _indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout):
"""Save information about the title and its location in the
file to the index.
"""
if aLine.startswith("# "):
hDepth = "H1"
hText = aLine[2:].strip()
@@ -382,7 +381,9 @@ class NWIndex():
return True
def indexWordCounts(self, tHandle, isNovel, theText, nTitle):
def _indexWordCounts(self, tHandle, isNovel, theText, nTitle):
"""Count text stats and save the counts to the index.
"""
cC, wC, pC = countWords(theText)
sTitle = "T%d" % nTitle
if isNovel:
@@ -401,7 +402,9 @@ class NWIndex():
self.noteIndex[tHandle][sTitle]["updated"] = time()
return
def indexSynopsis(self, tHandle, isNovel, theText, nTitle):
def _indexSynopsis(self, tHandle, isNovel, theText, nTitle):
"""Save the synopsis to the index.
"""
sTitle = "T%d" % nTitle
if isNovel:
if tHandle in self.novelIndex:
@@ -415,11 +418,10 @@ class NWIndex():
self.noteIndex[tHandle][sTitle]["updated"] = time()
return
def indexNoteRef(self, tHandle, aLine, nLine, nTitle):
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
"""Validate and save the information about a reference to a tag
in another file.
"""
isValid, theBits, thePos = self.scanThis(aLine)
if not isValid or len(theBits) == 0:
return False
@@ -435,10 +437,9 @@ class NWIndex():
return True
def indexTag(self, tHandle, aLine, nLine, itemClass):
def _indexTag(self, tHandle, aLine, nLine, itemClass):
"""Validate and save the information from a tag.
"""
isValid, theBits, thePos = self.scanThis(aLine)
if not isValid or len(theBits) != 2:
return False
@@ -453,42 +454,39 @@ class NWIndex():
##
def scanThis(self, aLine):
"""Scan a line starting with @ to check that it's valid and to
split up its elements into an array and an array of positions.
The latter is needed for the syntax highlighter.
"""Scan a line starting with @ to check that it's valid. Then
split it up into its elements and positions as two arrays.
"""
theBits = [] # The elements of the string
thePos = [] # The absolute position of each element
theBits = []
thePos = []
aLine = aLine.strip()
aLine = aLine.rstrip() # Remove all trailing white spaces
nChar = len(aLine)
if nChar < 2:
return False, theBits, thePos
if aLine[0] != "@":
return False, theBits, thePos
cPos = 0
cKey, cSep, cVals = aLine.partition(":")
cKey, _, cVals = aLine.partition(":")
sKey = cKey.strip()
if sKey == "@":
return False, theBits, thePos
cPos = 0
theBits.append(sKey)
thePos.append(cPos)
cPos += len(sKey) + 1
cPos += len(cKey) + 1
if cVals == "":
if not cVals:
# No values, so we're done
return True, theBits, thePos
aVals = cVals.split(",")
for cVal in aVals:
for cVal in cVals.split(","):
sVal = cVal.strip()
rLen = len(cVal.lstrip())
tLen = len(cVal)
theBits.append(sVal)
thePos.append(cPos+tLen-rLen)
thePos.append(cPos + tLen - rLen)
cPos += tLen + 1
return True, theBits, thePos
@@ -497,8 +495,7 @@ class NWIndex():
"""Check the tags against the index to see if they are valid
tags. This is needed for syntax highlighting.
"""
nBits = len(theBits)
nBits = len(theBits)
isGood = [False]*nBits
if nBits == 0:
return []
@@ -512,7 +509,7 @@ class NWIndex():
# is ignored
if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
isGood[0] = True
if theBits[1] in self.tagIndex.keys():
if theBits[1] in self.tagIndex:
if self.tagIndex[theBits[1]][1] == tItem.itemHandle:
isGood[1] = True
else:
@@ -537,7 +534,6 @@ class NWIndex():
order as they appear in the tree view and in the respective
document files, but skipping all note files.
"""
theStructure = []
for tHandle in self.theProject.projTree.handles():
if tHandle not in self.novelIndex:
@@ -551,7 +547,6 @@ class NWIndex():
"""Returns the counts for a file, or a section of a file
starting at title nTitle.
"""
cC = 0
wC = 0
pC = 0
@@ -579,7 +574,6 @@ class NWIndex():
"""Extract all references made in a file, and optionally title
section. sTitle must be a string.
"""
theRefs = {}
for tKey in self.TAG_CLASS:
theRefs[tKey] = []
@@ -602,7 +596,6 @@ class NWIndex():
"""Build a list of files referring back to our file, specified
by tHandle.
"""
theRefs = {}
tItem = self.theProject.projTree[tHandle]
+55 -10
View File
@@ -46,7 +46,7 @@ from nw.core.tools import projectMaintenance
from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import (
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
logger = logging.getLogger(__name__)
@@ -82,10 +82,9 @@ class NWProject():
self.bookTitle = "" # The final title; should only be used for exports
self.bookAuthors = [] # A list of book authors
# Various
self.autoReplace = {} # Text to auto-replace on exports
# Project Settings
self.autoReplace = {} # Text to auto-replace on exports
self.titleFormat = {} # The formatting of titles for exports
self.spellCheck = False # Controls the spellcheck-as-you-type feature
self.autoOutline = True # If true, the Project Outline is updated automatically
self.statusItems = None # Novel file progress status values
@@ -202,6 +201,16 @@ class NWProject():
self.bookTitle = ""
self.bookAuthors = []
self.autoReplace = {}
self.titleFormat = {
"title" : r"%title%",
"chapter" : r"Chapter %num%\\%title%",
"unnumbered" : r"%title%",
"scene" : r"* * *",
"section" : r"",
"withSynopsis" : False,
"withComments" : False,
"withKeywords" : False,
}
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
@@ -364,6 +373,11 @@ class NWProject():
elif xItem.tag == "autoReplace":
for xEntry in xItem:
self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False)
elif xItem.tag == "titleFormat":
titleFormat = self.titleFormat.copy()
for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
self.setTitleFormat(titleFormat)
elif xChild.tag == "content":
logger.debug("Found project content")
self.projTree.unpackXML(xChild)
@@ -435,10 +449,16 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
xAutoRep = etree.SubElement(xSettings, "autoReplace")
for aKey, aValue in self.autoReplace.items():
if len(aKey) > 0:
self._packProjectValue(xAutoRep,aKey,aValue)
self._packProjectValue(xAutoRep, aKey, aValue)
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
for aKey, aValue in self.titleFormat.items():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
xStatus = etree.SubElement(xSettings,"status")
self.statusItems.packEntries(xStatus)
@@ -527,11 +547,7 @@ class NWProject():
), nwAlert.WARN)
return False
cleanName = ""
for c in self.projName.strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
cleanName = self.getFileSafeProjectName()
baseDir = path.join(self.mainConf.backupPath, cleanName)
if not path.isdir(baseDir):
try:
@@ -713,6 +729,16 @@ class NWProject():
self.autoReplace = autoReplace
return
def setTitleFormat(self, titleFormat):
"""Set the formatting of titles in the project.
"""
for valKey, valEntry in titleFormat.items():
if valKey in ("title","chapter","unnumbered","scene","section"):
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
elif valKey in ("withSynopsis","withComments","withKeywords"):
self.titleFormat[valKey] = checkBool(valEntry, False, False)
return
def setProjectChanged(self, bValue):
"""Toggle the project changed flag, and propagate the
information to the GUI statusbar.
@@ -728,6 +754,15 @@ class NWProject():
# Getters
##
def getFileSafeProjectName(self):
"""Returns a filename safe version of the project name.
"""
cleanName = ""
for c in self.projName.strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
def getSessionWordCount(self):
"""Returns the number of words added or removed this session.
"""
@@ -1307,6 +1342,7 @@ class NWItem():
self.itemLayout = nwItemLayout.NO_LAYOUT
self.itemStatus = None
self.isExpanded = False
self.isExported = True
# Document Meta Data
self.charCount = 0
@@ -1334,6 +1370,7 @@ class NWItem():
xSub = self._subPack(xPack,"status", text=str(self.itemStatus))
xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded))
if self.itemType == nwItemType.FILE:
xSub = self._subPack(xPack,"exported", text=str(self.isExported))
xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name))
xSub = self._subPack(xPack,"charCount", text=str(self.charCount), none=False)
xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False)
@@ -1366,6 +1403,7 @@ class NWItem():
"layout" : self.setLayout,
"status" : self.setStatus,
"expanded" : self.setExpanded,
"exported" : self.setExported,
"charCount" : self.setCharCount,
"wordCount" : self.setWordCount,
"paraCount" : self.setParaCount,
@@ -1466,6 +1504,13 @@ class NWItem():
self.isExpanded = expState == True
return
def setExported(self, expState):
if isinstance(expState, str):
self.isExported = expState == str(True)
else:
self.isExported = expState == True
return
##
# Set Document Meta Data
##
+266
View File
@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
"""novelWriter HTML Text Converter
novelWriter HTML Text Converter
===================================
Extends the Tokenizer class to write HTML
File History:
Created: 2019-05-07 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from nw.core.tokenizer import Tokenizer
from nw.constants import nwUnicode, nwLabels, nwKeyWords
logger = logging.getLogger(__name__)
class ToHtml(Tokenizer):
M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_EXPORT
self.repDict = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"\t" : "&emsp;",
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
}
return
##
# Setters
##
def setPreview(self, forPreview, doComments):
"""If we're using this class to generate markdown preview, we
need to make a few changes to formatting, which is managed by
these flags.
"""
if forPreview:
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.repDict["\t"] = "&nbsp;"*8
return
##
# Class Methods
##
def doAutoReplace(self):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doAutoReplace(self)
xRep = re.compile("|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: self.repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
"""Reverse the html entities replacement on the markdown text.
Otherwise, all the &something; bits will also be in there.
"""
if self.genMode == self.M_PREVIEW:
# Doesn't matter for preview as we don't use the markdown
return
revDict = dict(map(reversed, self.repDict.items()))
xRep = re.compile("|".join([re.escape(k) for k in revDict.keys()]), flags=re.DOTALL)
self.theMarkdown = xRep.sub(lambda x: revDict[x.group(0)], self.theMarkdown)
return
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
htmlTags = {
self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>",
self.FMT_I_B : "<em>",
self.FMT_I_E : "</em>",
self.FMT_U_B : "<u>",
self.FMT_U_E : "</u>",
}
self.theResult = ""
thisPar = []
parStyle = ""
tmpResult = []
for tType, tText, tFormat, tStyle in self.theTokens:
# Styles
aStyle = []
if tStyle is not None:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
if tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
if tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
if tStyle & self.A_JUSTIFY:
aStyle.append("text-align: justify;")
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
if tStyle & self.A_PBB_L:
aStyle.append("page-break-before: left;")
if tStyle & self.A_PBB_R:
aStyle.append("page-break-before: right;")
if tStyle & self.A_PBB_AV:
aStyle.append("page-break-before: avoid;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
if tStyle & self.A_PBA_L:
aStyle.append("page-break-after: left;")
if tStyle & self.A_PBA_R:
aStyle.append("page-break-after: right;")
if tStyle & self.A_PBA_AV:
aStyle.append("page-break-after: avoid;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
else:
hStyle = ""
# Process TextType
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "".join(thisPar)
tmpResult.append("<p%s>%s</p>\n" % (parStyle, tTemp.rstrip()))
thisPar = []
parStyle = ""
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h1%s>%s</h1>\n" % (hStyle, tHead))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h2%s>%s</h2>\n" % (hStyle, tHead))
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h3%s>%s</h3>\n" % (hStyle, tHead))
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<h4%s>%s</h4>\n" % (hStyle, tHead))
elif tType == self.T_SEP:
tmpResult.append("<p%s>%s</p>\n" % (hStyle, tText))
elif tType == self.T_SKIP:
tmpResult.append("<p%s>&nbsp;</p>\n" % hStyle)
elif tType == self.T_TEXT:
tTemp = tText
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>")
else:
thisPar.append(tTemp.rstrip()+" ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText))
self.theResult = "".join(tmpResult)
tmpResult = []
return
##
# Internal Functions
##
def _formatSynopsis(self, tText):
"""Apply HTML formatting to synopsis.
"""
if self.genMode == self.M_EXPORT:
return "<p class='synopsis'><strong>Synopsis: </strong>%s</p>\n" % tText
else:
return "<p class='comment'>%s</p>\n" % tText
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
if self.genMode == self.M_EXPORT:
return "<p class='comment'><strong>Comment: </strong>%s</p>\n" % tText
else:
return "<p class='comment'>%s</p>\n" % tText
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
tText = "@"+tText
isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
retText += "<span class='tags'>%s:</span>&nbsp;" % nwLabels.KEY_NAME[theBits[0]]
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theBits[0][1:], tTag, tTag
))
retText += ", ".join(refTags)
else:
if theBits[0] == nwKeyWords.TAG_KEY:
retText += "<a name='tag_%s'/>%s" % (
theBits[1], theBits[1]
)
else:
for tTag in theBits[1:]:
refTags.append("<a href='#tag_%s'>%s</a>" % (
tTag, tTag
))
retText += ", ".join(refTags)
return "<div>%s</div>" % retText
# END Class ToHtml
+533
View File
@@ -0,0 +1,533 @@
# -*- coding: utf-8 -*-
"""novelWriter Text Tokenizer
novelWriter Text Tokenizer
==============================
Splits a piece of nW markdown text into its elements
File History:
Created: 2019-05-05 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
import nw
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord
from nw.constants import nwItemLayout
logger = logging.getLogger(__name__)
class Tokenizer():
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_U_B = 5 # Begin underline
FMT_U_E = 6 # End underline
T_EMPTY = 1 # Empty line (new paragraph)
T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line
T_HEAD1 = 5 # Header 1 (title)
T_HEAD2 = 6 # Header 2 (chapter)
T_HEAD3 = 7 # Header 3 (scene)
T_HEAD4 = 8 # Header 4
T_TEXT = 9 # Text line
T_SEP = 10 # Scene separator
T_SKIP = 11 # Paragraph break
A_LEFT = 1 # Left aligned
A_RIGHT = 2 # Right aligned
A_CENTRE = 4 # Centred
A_JUSTIFY = 8 # Justified
A_PBB = 16 # Page break before
A_PBB_L = 32 # Page break before, left
A_PBB_R = 64 # Page break before, right
A_PBB_AV = 128 # Page break, avoid
A_PBA = 256 # Page break after
A_PBA_L = 512 # Page break after, left
A_PBA_R = 1024 # Page break after, right
A_PBA_AV = 2048 # Page break, avoid
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
# Data Variables
self.theText = None # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
self.theTokens = None # The list of the processed tokens
self.theResult = None # The result text after conversion
self.theMarkdown = None # The result text in novelWriter markdown
# User Settings
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
self.doJustify = False # Justify text
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
self.fmtScene = "%title%" # Formatting for scenes
self.fmtSection = "%title%" # Formatting for sections
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
# This File
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
return
def clearData(self):
"""Clear the data arrays and variables, but not settings, so the class
can be reused for multiple documents.
"""
self.theText = None
self.theHandle = None
self.theItem = None
self.theTokens = None
self.theResult = None
self.theMarkdown = None
self.numChapter = 0
self.firstScene = False
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
return
##
# Setters
##
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum
return
def setSceneFormat(self, fmtScene, hideScene):
self.fmtScene = fmtScene
self.hideScene = hideScene
return
def setSectionFormat(self, fmtSection, hideSection):
self.fmtSection = fmtSection
self.hideSection = hideSection
return
def setBodyText(self, doBodyText):
self.doBodyText = doBodyText
return
def setSynopsis(self, doSynopsis):
self.doSynopsis = doSynopsis
return
def setComments(self, doComments):
self.doComments = doComments
return
def setKeywords(self, doKeywords):
self.doKeywords = doKeywords
return
def setJustify(self, doJustify):
self.doJustify = doJustify
return
##
# Class Methods
##
def setText(self, theHandle, theText=None):
"""Set the text for the tokenizer from a handle. If theText is
not set, load it from the file.
"""
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if theText is not None:
# If the text is set, just use that
self.theText = theText
else:
# Otherwise, load it from file
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK
self.isPage = self.theItem.itemLayout == nwItemLayout.PAGE
self.isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
self.isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
self.isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
self.isScene = self.theItem.itemLayout == nwItemLayout.SCENE
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
return
def getResult(self):
"""Return the result from the conversion.
"""
return self.theResult
def getFilteredMarkdown(self):
"""Return the novelWriter markdown after the filters have been applied.
"""
return self.theMarkdown
def doAutoReplace(self):
"""Run through the user's auto-replace dictionary.
"""
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
repDict["<%s>" % aKey] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
return
def doPostProcessing(self):
"""Do some postprocessing. Overloaded by subclasses.
"""
return
def tokenizeText(self):
"""Scan the text for either lines starting with specific
characters that indicate headers, comments, commands etc, or
just contains plain text. in the case of plain text, apply the
same RegExes that the syntax highlighter uses and save the
locations of these formatting tags into the token array.
The format of the token list is an entry with a four-tuple for
each line in the file. The tuple is as follows:
1: The type of the block, self.T_*
2: The text content of the block, without leading tags
3: The internal formatting map of the text, self.FMT_*
4: The style of the block, self.A_*
"""
# RegExes for adding formatting tags within text lines
# Keep in sync with the DocHighlighter class
rxFormats = [(
QRegularExpression(r"(?<![\w|\\])([\*]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_B_B, None, self.FMT_B_E]
),(
QRegularExpression(r"(?<![\w|_|\\])([_])(?!\s|\1)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_I_B, None, self.FMT_I_E]
),(
QRegularExpression(r"(?<![\w|\\])([_]{2})(?!\s)(?m:(.+?))(?<![\s|\\])(\1)(?!\w)"),
[None, self.FMT_U_B, None, self.FMT_U_E]
)]
if self.doJustify:
defAlign = self.A_JUSTIFY
else:
defAlign = self.A_LEFT
self.theTokens = []
self.theMarkdown = ""
tmpMarkdown = []
for aLine in self.theText.splitlines():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
self.T_EMPTY, "", None, None
))
tmpMarkdown.append("\n")
elif aLine[0] == "%":
cLine = aLine[1:].strip()
if cLine.lower().startswith("synopsis:"):
self.theTokens.append((
self.T_SYNOPSIS, cLine[9:].strip(), None, defAlign
))
if self.doSynopsis:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT, aLine[1:].strip(), None, defAlign
))
if self.doComments:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD, aLine[1:].strip(), None, self.A_LEFT
))
if self.doKeywords:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
self.T_HEAD1, aLine[2:].strip(), None, self.A_LEFT | self.A_PBB
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
self.T_HEAD2, aLine[3:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3, aLine[4:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4, aLine[5:].strip(), None, self.A_LEFT | self.A_PBA_AV
))
tmpMarkdown.append("%s\n" % aLine)
else:
if not self.doBodyText:
# Skip all body text
continue
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
for theRX, theKeys in rxFormats:
rxThis = theRX.globalMatch(aLine, 0)
while rxThis.hasNext():
rxMatch = rxThis.next()
for n in range(1,len(theKeys)):
if theKeys[n] is not None:
xPos = rxMatch.capturedStart(n)
xLen = rxMatch.capturedLength(n)
fmtPos.append([xPos,xLen,theKeys[n]])
# Save the line as is, but append the array of formatting locations
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self.T_TEXT, aLine, fmtPos, defAlign
))
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
self.T_EMPTY, "", None, None
))
tmpMarkdown.append("\n")
self.theMarkdown = "".join(tmpMarkdown)
tmpMarkdown = []
return
def doHeaders(self):
"""Apply formatting to the text headers according to document
layout and user settings.
"""
# No special header formatting for notes and no-layout files
if self.isNone or self.isNote:
return
# For novel files, we need to handle chapter numbering and scene
# breaks
if self.isNovel:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
# In case we see text before a scene, we reset the flag
if tType == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD1:
# Main Title
# ==========
tText = self._formatHeading(self.fmtTitle, tText)
self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R
)
elif tType == self.T_HEAD2:
# Novel Chapter
# =============
# Numbered or Unnumbered
if self.isUnNum:
tText = self._formatHeading(self.fmtUnNum, tText)
else:
self.numChapter += 1
tText = self._formatHeading(self.fmtChapter, tText)
# Format the chapter header
self.theTokens[n] = (
tType, tText, None, self.A_LEFT | self.A_PBB_R
)
# Set scene variables
self.firstScene = True
self.numChScene = 0
elif tType == self.T_HEAD3:
# Novel Scene
# ===========
self.numChScene += 1
self.numAbsScene += 1
tTemp = self._formatHeading(self.fmtScene, tText)
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
else:
self.theTokens[n] = (
self.T_SKIP, "", None, None
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
else:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV
)
# Definitely no longer the first scene
self.firstScene = False
elif tType == self.T_HEAD4:
# Novel Section
# =============
tTemp = self._formatHeading(self.fmtSection, tText)
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY, "", None, None
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP, "", None, None
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP, tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tType, tTemp, None, self.A_LEFT | self.A_PBA_AV
)
# For title page and partitions, we need to centre all text.
# For partition, we also add a page break before, and for
# both types we always add a page break after the content.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
if self.isTitle:
self.theTokens[n] = (
tType, tText, tFormat, self.A_CENTRE
)
# Add a page break after the last entry
n = len(self.theTokens) - 1
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA
)
return
##
# Internal Functions
##
def _formatHeading(self, theTitle, theText):
"""Replaces the %keyword% strings.
"""
theTitle = theTitle.replace(r"%title%", theText)
theTitle = theTitle.replace(r"%chnum%", str(self.numChapter))
theTitle = theTitle.replace(r"%scnum%", str(self.numChScene))
theTitle = theTitle.replace(r"%scabsnum%", str(self.numAbsScene))
theTitle = theTitle.replace(r"%chnumword%", numberToWord(self.numChapter,"en"))
return theTitle
# END Class Tokenizer
+4 -8
View File
@@ -1,20 +1,17 @@
# -*- coding: utf-8 -*-
# Qt Additions
from nw.gui.additions.qconfiglayout import QConfigLayout
from nw.gui.additions.qswitch import QSwitch
# Main Window Elements
from nw.gui.build import GuiBuildNovel
from nw.gui.icons import GuiIcons
from nw.gui.mainmenu import GuiMainMenu
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiTheme
# Dialogs
from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
@@ -37,16 +34,15 @@ from nw.gui.tools.optionstate import OptionState
from nw.gui.tools.wordcounter import WordCounter
__all__ = [
"QConfigLayout",
"QSwitch",
"GuiBuildNovel",
"GuiIcons",
"GuiMainMenu",
"GuiMainStatus",
"GuiTheme",
"GuiAbout",
"GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
"GuiProjectLoad",
+4
View File
@@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
from nw.gui.additions.pageddialog import PagedDialog
from nw.gui.additions.qconfiglayout import QConfigLayout
from nw.gui.additions.qconfiglayout import QHelpLabel
from nw.gui.additions.qswitch import QSwitch
__all__ = [
"PagedDialog",
"QConfigLayout",
"QHelpLabel",
"QSwitch",
]
+117
View File
@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
"""novelWriter Paged Dialog
novelWriter Paged Dialog
============================
A custom QDialog with a built-in QTabWidget and vertical tabs.
File History:
Created: 2020-05-17 [0.5.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from PyQt5.QtCore import Qt, QRect, QPoint
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QTabWidget, QTabBar, QStyle,
QStylePainter, QStyleOptionTab
)
logger = logging.getLogger(__name__)
class PagedDialog(QDialog):
def __init__(self, theParent=None):
QDialog.__init__(self, parent=theParent)
self._outerBox = QVBoxLayout()
self._buttonBox = QHBoxLayout()
self._tabBox = QTabWidget()
self._tabBar = VerticalTabBar(self)
self._tabBox.setTabBar(self._tabBar)
self._tabBox.setTabPosition(QTabWidget.West)
self._tabBar.setExpanding(False)
self._outerBox.addWidget(self._tabBox)
self._outerBox.addLayout(self._buttonBox)
self.setLayout(self._outerBox)
# Default Margins
qM = self._outerBox.contentsMargins()
mL = qM.left()
mR = qM.right()
mT = qM.top()
mB = qM.bottom()
self.setContentsMargins(0, 0, 0, 0)
self._outerBox.setContentsMargins(0, 0, 0, mB)
self._buttonBox.setContentsMargins(mL, 0, mR, 0)
self._outerBox.setSpacing(mT)
return
def addTab(self, tabWidget, tabLabel):
self._tabBox.addTab(tabWidget, tabLabel)
return
def addControls(self, buttonBar):
self._buttonBox.addWidget(buttonBar)
return
# END Class PagedDialog
class VerticalTabBar(QTabBar):
def __init__(self, theParent=None):
QTabBar.__init__(self, parent=theParent)
return
def tabSizeHint(self, theIndex):
tSize = QTabBar.tabSizeHint(self, theIndex)
tSize.transpose()
return tSize
def paintEvent(self, theEvent):
pObj = QStylePainter(self)
oObj = QStyleOptionTab()
for i in range(self.count()):
self.initStyleOption(oObj, i)
pObj.drawControl(QStyle.CE_TabBarTabShape, oObj)
pObj.save()
oSize = oObj.rect.size()
oSize.transpose()
oRect = QRect(QPoint(), oSize)
oRect.moveCenter(oObj.rect.center())
oObj.rect = oRect
oCenter = self.tabRect(i).center()
pObj.translate(oCenter)
pObj.rotate(90)
pObj.translate(-oCenter)
pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj)
pObj.restore()
return
# END Class VerticalTabBar
+23 -9
View File
@@ -130,17 +130,9 @@ class QConfigLayout(QGridLayout):
qLabel.setIndent(8)
if helpText is not None:
qHelp = QLabel(str(helpText))
qHelp = QHelpLabel(str(helpText), self._helpCol, self._fontScale)
qHelp.setIndent(8)
lblCol = qHelp.palette()
lblCol.setColor(QPalette.WindowText, self._helpCol)
qHelp.setPalette(lblCol)
lblFont = qHelp.font()
lblFont.setPointSizeF(self._fontScale*lblFont.pointSizeF())
qHelp.setFont(lblFont)
labelBox = QVBoxLayout()
labelBox.addWidget(qLabel)
labelBox.addWidget(qHelp)
@@ -176,3 +168,25 @@ class QConfigLayout(QGridLayout):
return self._nextRow - 1
# END Class QConfigLayout
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
QLabel.__init__(self, theText)
if isinstance(textCol, QColor):
qCol = textCol
else:
qCol = QColor(*textCol)
lblCol = self.palette()
lblCol.setColor(QPalette.WindowText, qCol)
self.setPalette(lblCol)
lblFont = self.font()
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
self.setFont(lblFont)
return
# END Class QHelpLabel
+691
View File
@@ -0,0 +1,691 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Build Novel
novelWriter GUI Build Novel
===============================
Class holding the build novel window
File History:
Created: 2020-05-09 [0.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from time import time
from PyQt5.QtCore import Qt, QByteArray
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
QTextOption, QPalette, QColor, QTextDocumentWriter
)
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, QFileDialog
)
from nw.gui.additions import QSwitch
from nw.core import ToHtml
from nw.constants import (
nwConst, nwFiles, nwAlert, nwItemType, nwItemLayout, nwItemClass
)
logger = logging.getLogger(__name__)
class GuiBuildNovel(QDialog):
FMT_ODT = 1
FMT_PDF = 2
FMT_HTM = 3
FMT_MD = 4
FMT_NWD = 5
FMT_TXT = 6
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiBuildNovel ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
self.optState = self.theProject.optState
self.htmlText = [] # List of html document
self.nwdText = [] # List of markdown documents
self.textLayout = [] # List of nwItemLayout entries
self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(800)
self.setMinimumHeight(800)
self.resize(
self.optState.getInt("GuiBuildNovel", "winWidth", 800),
self.optState.getInt("GuiBuildNovel", "winHeight", 800)
)
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.toolsBox = QVBoxLayout()
self.docView = GuiBuildNovelDocView(self, self.theProject)
# Title Formats
# =============
self.titleGroup = QGroupBox("Novel Title Formats", self)
self.titleForm = QGridLayout(self)
self.titleGroup.setLayout(self.titleForm)
self.fmtTitle = QLineEdit()
self.fmtTitle.setMaxLength(200)
self.fmtTitle.setFixedWidth(200)
self.fmtTitle.setText(self.theProject.titleFormat["title"])
self.fmtChapter = QLineEdit()
self.fmtChapter.setMaxLength(200)
self.fmtChapter.setFixedWidth(200)
self.fmtChapter.setText(self.theProject.titleFormat["chapter"])
self.fmtUnnumbered = QLineEdit()
self.fmtUnnumbered.setMaxLength(200)
self.fmtUnnumbered.setFixedWidth(200)
self.fmtUnnumbered.setText(self.theProject.titleFormat["unnumbered"])
self.fmtScene = QLineEdit()
self.fmtScene.setMaxLength(200)
self.fmtScene.setFixedWidth(200)
self.fmtScene.setText(self.theProject.titleFormat["scene"])
self.fmtSection = QLineEdit()
self.fmtSection.setMaxLength(200)
self.fmtSection.setFixedWidth(200)
self.fmtSection.setText(self.theProject.titleFormat["section"])
self.titleForm.addWidget(QLabel("Title"), 0, 0)
self.titleForm.addWidget(self.fmtTitle, 0, 1)
self.titleForm.addWidget(QLabel("Chapter"), 1, 0)
self.titleForm.addWidget(self.fmtChapter, 1, 1)
self.titleForm.addWidget(QLabel("Unnumbered"), 2, 0)
self.titleForm.addWidget(self.fmtUnnumbered, 2, 1)
self.titleForm.addWidget(QLabel("Scene"), 3, 0)
self.titleForm.addWidget(self.fmtScene, 3, 1)
self.titleForm.addWidget(QLabel("Section"), 4, 0)
self.titleForm.addWidget(self.fmtSection, 4, 1)
self.titleForm.setColumnStretch(0, 1)
self.titleForm.setColumnStretch(1, 0)
# Text Options
# =============
self.textGroup = QGroupBox("Text Options", self)
self.textForm = QGridLayout(self)
self.textGroup.setLayout(self.textForm)
self.justifyText = QSwitch()
self.justifyText.setChecked(self.optState.getBool("GuiBuildNovel", "justifyText", False))
self.textForm.addWidget(QLabel("Justify text"), 0, 0)
self.textForm.addWidget(self.justifyText, 0, 1)
self.textForm.setColumnStretch(0, 1)
self.textForm.setColumnStretch(1, 0)
# Include Switches
# ================
self.includeGroup = QGroupBox("Include Non-Text Elements", self)
self.includeForm = QGridLayout(self)
self.includeGroup.setLayout(self.includeForm)
self.includeSynopsis = QSwitch()
self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"])
self.includeComments = QSwitch()
self.includeComments.setChecked(self.theProject.titleFormat["withComments"])
self.includeKeywords = QSwitch()
self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"])
self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0)
self.includeForm.addWidget(self.includeSynopsis, 0, 1)
self.includeForm.addWidget(QLabel("Include comments"), 1, 0)
self.includeForm.addWidget(self.includeComments, 1, 1)
self.includeForm.addWidget(QLabel("Include keywords"), 2, 0)
self.includeForm.addWidget(self.includeKeywords, 2, 1)
self.includeForm.setColumnStretch(0, 1)
self.includeForm.setColumnStretch(1, 0)
# Additional Options
# ==================
self.addsGroup = QGroupBox("Additional Options", self)
self.addsForm = QGridLayout(self)
self.addsGroup.setLayout(self.addsForm)
self.novelFiles = QSwitch()
self.novelFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNovel", True))
self.noteFiles = QSwitch()
self.noteFiles.setChecked(self.optState.getBool("GuiBuildNovel", "addNotes", False))
self.ignoreFlag = QSwitch()
self.ignoreFlag.setChecked(self.optState.getBool("GuiBuildNovel", "ignoreFlag", False))
self.excludeBody = QSwitch()
self.excludeBody.setChecked(self.optState.getBool("GuiBuildNovel", "excludeBody", False))
self.addsForm.addWidget(QLabel("Include novel files"), 0, 0)
self.addsForm.addWidget(self.novelFiles, 0, 1)
self.addsForm.addWidget(QLabel("Include note files"), 1, 0)
self.addsForm.addWidget(self.noteFiles, 1, 1)
self.addsForm.addWidget(QLabel("Ignore export flag"), 2, 0)
self.addsForm.addWidget(self.ignoreFlag, 2, 1)
self.addsForm.addWidget(QLabel("Exclude body text"), 3, 0)
self.addsForm.addWidget(self.excludeBody, 3, 1)
self.addsForm.setColumnStretch(0, 1)
self.addsForm.setColumnStretch(1, 0)
# Build Button
# ============
self.buildProgress = QProgressBar()
self.genPreview = QPushButton("Generate Preview")
self.genPreview.clicked.connect(self._buildPreview)
# Action Buttons
# ==============
self.buttonForm = QGridLayout()
self.btnHelp = QPushButton("Help")
self.btnHelp.clicked.connect(self._showHelp)
self.btnPrint = QPushButton("Print")
self.btnPrint.clicked.connect(self._printDocument)
self.btnSave = QPushButton("Save As")
self.saveMenu = QMenu(self)
self.btnSave.setMenu(self.saveMenu)
self.saveODT = QAction("Open Document (.odt)")
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
self.saveMenu.addAction(self.saveODT)
self.savePDF = QAction("Portable Document Format (.pdf)")
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF)
self.saveHTM = QAction("%s HTML (.htm)" % nw.__package__)
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
self.saveMenu.addAction(self.saveHTM)
if self.mainConf.verQtValue >= 51400:
self.saveMD = QAction("Markdown (.md)")
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
self.saveMenu.addAction(self.saveMD)
self.saveNWD = QAction("novelWriter Markdown (.nwd)")
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
self.saveTXT = QAction("Plain Text (.txt)")
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT)
self.btnClose = QPushButton("Close")
self.btnClose.clicked.connect(self._doClose)
self.buttonForm.addWidget(self.btnHelp, 0, 0)
self.buttonForm.addWidget(self.btnPrint, 0, 1)
self.buttonForm.addWidget(self.btnSave, 1, 0)
self.buttonForm.addWidget(self.btnClose, 1, 1)
# Assemble GUI
# ============
self.toolsBox.addWidget(self.titleGroup)
self.toolsBox.addWidget(self.textGroup)
self.toolsBox.addWidget(self.includeGroup)
self.toolsBox.addWidget(self.addsGroup)
self.toolsBox.addStretch(1)
self.toolsBox.addWidget(self.buildProgress)
self.toolsBox.addWidget(self.genPreview)
self.toolsBox.addSpacing(8)
self.toolsBox.addLayout(self.buttonForm)
self.innerBox.addLayout(self.toolsBox)
self.innerBox.addWidget(self.docView)
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
self.innerBox.setStretch(0, 0)
self.innerBox.setStretch(1, 1)
self.show()
logger.debug("GuiBuildNovel initialisation complete")
return
##
# Slots
##
def _buildPreview(self):
"""Build a preview of the project in the document viewer.
"""
# Get Settings
fmtTitle = self.fmtTitle.text().strip()
fmtChapter = self.fmtChapter.text().strip()
fmtUnnumbered = self.fmtUnnumbered.text().strip()
fmtScene = self.fmtScene.text().strip()
fmtSection = self.fmtSection.text().strip()
justifyText = self.justifyText.isChecked()
incSynopsis = self.includeSynopsis.isChecked()
incComments = self.includeComments.isChecked()
incKeywords = self.includeKeywords.isChecked()
novelFiles = self.novelFiles.isChecked()
noteFiles = self.noteFiles.isChecked()
ignoreFlag = self.ignoreFlag.isChecked()
excludeBody = self.excludeBody.isChecked()
makeHtml = ToHtml(self.theProject, self.theParent)
makeHtml.setTitleFormat(fmtTitle)
makeHtml.setChapterFormat(fmtChapter)
makeHtml.setUnNumberedFormat(fmtUnnumbered)
makeHtml.setSceneFormat(fmtScene, fmtScene == "")
makeHtml.setSectionFormat(fmtSection, fmtSection == "")
makeHtml.setBodyText(not excludeBody)
makeHtml.setSynopsis(incSynopsis)
makeHtml.setComments(incComments)
makeHtml.setKeywords(incKeywords)
makeHtml.setJustify(justifyText)
self.buildProgress.setMaximum(len(self.theProject.projTree))
self.buildProgress.setValue(0)
tStart = time()
self.htmlText = []
self.nwdText = []
self.textLayout = []
for nItt, tItem in enumerate(self.theProject.projTree):
if self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
makeHtml.setText(tItem.itemHandle)
makeHtml.doAutoReplace()
makeHtml.tokenizeText()
makeHtml.doHeaders()
makeHtml.doConvert()
makeHtml.doPostProcessing()
self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown())
self.textLayout.append(tItem.itemLayout)
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
tEnd = time()
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
# Load the preview document with the html data
self.docView.setHtml("".join(self.htmlText))
return
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag):
"""This function checks whether a file should be included in the
export or not. For standard note and novel files, this is
controlled by the options selected by the user. For other files
classified as non-exportable, a few checks must be made, and the
following are not:
* Items that are not actual files.
* Items that have been orphaned which are tagged as NO_LAYOUT
and NO_CLASS.
* Items that appear in the TRASH folder or have parent set to
None (orphaned files).
"""
if theItem is None:
return False
if not theItem.isExported and not ignoreFlag:
return False
isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot()
isNone |= theItem.parHandle is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
if isNone:
return False
if isNote and not noteFiles:
return False
if isNovel and not novelFiles:
return False
return True
def _saveDocument(self, theFormat):
"""Save the document to various formats.
"""
# FMT_PDF
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
outTool = ""
# Create the settings
if theFormat == self.FMT_ODT:
byteFmt.append("odf")
fileExt = "odt"
textFmt = "Open Document"
outTool = "Qt"
elif theFormat == self.FMT_PDF:
fileExt = "pdf"
textFmt = "PDF"
outTool = "QtPrint"
elif theFormat == self.FMT_HTM:
fileExt = "htm"
textFmt = "Plain HTML"
outTool = "NW"
elif theFormat == self.FMT_MD:
byteFmt.append("markdown")
fileExt = "md"
textFmt = "Markdown"
outTool = "Qt"
elif theFormat == self.FMT_NWD:
fileExt = "nwd"
textFmt = "%s markdown" % nw.__package__
outTool = "NW"
elif theFormat == self.FMT_TXT:
byteFmt.append("plaintext")
fileExt = "txt"
textFmt = "Plain Text"
outTool = "Qt"
else:
return False
# Generate the file name
if fileExt:
cleanName = self.theProject.getFileSafeProjectName()
fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath
savePath = path.join(saveDir, fileName)
if not path.isdir(saveDir):
saveDir = self.mainConf.homePath
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo[0]:
savePath = saveTo[0]
else:
return False
self.mainConf.setLastPath(savePath)
else:
return False
# Do the actual writing
if outTool == "Qt":
docWriter = QTextDocumentWriter()
docWriter.setFileName(savePath)
docWriter.setFormat(byteFmt)
if docWriter.write(self.docView.qDocument):
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, savePath
), nwAlert.ERROR
)
elif outTool == "NW":
try:
with open(savePath, mode="w", encoding="utf8") as outFile:
if theFormat == self.FMT_HTM:
# Write novelWriter HTML data
outFile.write("<!DOCTYPE html>\n")
outFile.write("<html>\n")
outFile.write("<head>\n")
outFile.write("<meta charset='utf-8'>\n")
outFile.write("</head>\n")
outFile.write("<body>\n")
outFile.write("<article style='width: 800px; margin: 40px auto'>\n")
for aLine in self.htmlText:
outFile.write(aLine)
outFile.write("</article>\n")
outFile.write("</body>\n")
outFile.write("</html>\n")
elif theFormat == self.FMT_NWD:
# Write novelWriter markdown data
for aLine in self.nwdText:
outFile.write(aLine)
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
elif outTool == "QtPrint" and theFormat == self.FMT_PDF:
try:
thePrinter = QPrinter()
thePrinter.setOutputFormat(QPrinter.PdfFormat)
thePrinter.setOrientation(QPrinter.Portrait)
thePrinter.setDuplex(QPrinter.DuplexLongSide)
thePrinter.setFontEmbeddingEnabled(True)
thePrinter.setColorMode(QPrinter.Color)
thePrinter.setOutputFileName(savePath)
self.docView.qDocument.print(thePrinter)
self.theParent.makeAlert(
"Document successfully written in %s format to file: %s" % (
textFmt, savePath
), nwAlert.INFO
)
except Exception as e:
self.theParent.makeAlert(
"Failed to write document in %s format to file: %s" % (
textFmt, str(e)
), nwAlert.ERROR
)
else:
return False
return True
def _printDocument(self):
"""Open the print preview dialog.
"""
thePreview = QPrintPreviewDialog(self)
thePreview.paintRequested.connect(self._doPrintPreview)
thePreview.exec_()
return
def _doPrintPreview(self, thePrinter):
"""Connect the print preview painter to the document viewer.
"""
thePrinter.setOrientation(QPrinter.Portrait)
thePrinter.setOutputFormat(QPrinter.NativeFormat | QPrinter.PdfFormat)
self.docView.qDocument.print(thePrinter)
return
def _doClose(self):
"""Close button was clicked.
"""
self.close()
return
##
# Events
##
def closeEvent(self, theEvent):
"""Capture the user closing the window so we can save settings.
"""
self._saveSettings()
QDialog.closeEvent(self, theEvent)
return
##
# Internal Functions
##
def _saveSettings(self):
"""Save the various user settings.
"""
logger.debug("Saving GuiBuildNovel settings")
# Formatting
self.theProject.setTitleFormat({
"title" : self.fmtTitle.text().strip(),
"chapter" : self.fmtChapter.text().strip(),
"unnumbered" : self.fmtUnnumbered.text().strip(),
"scene" : self.fmtScene.text().strip(),
"section" : self.fmtSection.text().strip(),
"withSynopsis" : self.includeSynopsis.isChecked(),
"withComments" : self.includeComments.isChecked(),
"withKeywords" : self.includeKeywords.isChecked(),
})
# GUI Settings
self.optState.setValue("GuiBuildNovel", "winWidth", self.width())
self.optState.setValue("GuiBuildNovel", "winHeight", self.height())
self.optState.setValue("GuiBuildNovel", "justifyText", self.justifyText.isChecked())
self.optState.setValue("GuiBuildNovel", "addNovel", self.novelFiles.isChecked())
self.optState.setValue("GuiBuildNovel", "addNotes", self.noteFiles.isChecked())
self.optState.setValue("GuiBuildNovel", "ignoreFlag", self.ignoreFlag.isChecked())
self.optState.setValue("GuiBuildNovel", "excludeBody", self.excludeBody.isChecked())
self.optState.saveSettings()
return
def _showHelp(self):
"""Generate a help text and show it in the document window.
"""
docName = "exportHelp_%s.htm" % self.mainConf.guiLang
docPath = path.join(self.mainConf.assetPath, "text", docName)
if path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.docView.setText(helpText)
else:
self.theParent.makeAlert(
"Could not open help text file for Build Project.", nwAlert.ERROR
)
return
# END Class GuiBuildNovel
class GuiBuildNovelDocView(QTextBrowser):
def __init__(self, theParent, theProject):
QTextBrowser.__init__(self, theParent)
logger.debug("Initialising GuiBuildNovelDocView ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.setMinimumWidth(400)
self.setOpenExternalLinks(False)
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
theOpt = QTextOption()
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
self.qDocument.setDefaultTextOption(theOpt)
docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
docPalette.setColor(QPalette.Text, QColor( 0, 0, 0))
self.setPalette(docPalette)
self._makeStyleSheet()
self.show()
logger.debug("GuiBuildNovelDocView initialisation complete")
return
def setText(self, theText):
self.setHtml(theText)
return
##
# Internal Functions
##
def _makeStyleSheet(self):
styleSheet = (
"h1, h2 {"
" color: rgb(66, 113, 174);"
"}\n"
"h3, h4 {"
" color: rgb(50, 50, 50);"
"}\n"
"a {"
" color: rgb(137, 89, 168);"
"}\n"
"mark {"
" background-color: rgb(240, 198, 116);"
"}\n"
".tags {"
" color: rgb(245, 135, 31);"
" font-wright: bold;"
"}\n"
)
self.qDocument.setDefaultStyleSheet(styleSheet)
return
# END Class GuiBuildNovelDocView
+2 -2
View File
@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
from nw.gui.dialogs.about import GuiAbout
from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView
__all__ = [
"GuiAbout",
"GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport",
"GuiItemEditor",
"GuiProjectEditor",
"GuiProjectLoad",
+232
View File
@@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI About Box
novelWriter GUI About Box
=============================
The about novelWriter dialog box
File History:
Created: 2020-05-21 [0.5.2]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
QTextBrowser, QLabel
)
logger = logging.getLogger(__name__)
class GuiAbout(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiAbout ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.innerBox.setSpacing(16)
self.setWindowTitle("About %s" % nw.__package__)
self.setMinimumWidth(700)
self.setMinimumHeight(600)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (96, 96))
self.lblName = QLabel("<b>%s</b>" % nw.__package__)
self.lblVers = QLabel("v%s" % nw.__version__)
self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x"))
self.leftBox = QVBoxLayout()
self.leftBox.setSpacing(4)
self.leftBox.addWidget(self.guiDeco, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblName, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblVers, 0, Qt.AlignCenter)
self.leftBox.addWidget(self.lblDate, 0, Qt.AlignCenter)
self.leftBox.addStretch(1)
self.innerBox.addLayout(self.leftBox)
# Pages
self.pageAbout = QTextBrowser()
self.pageAbout.setOpenExternalLinks(True)
self.pageAbout.document().setDocumentMargin(16)
# self.pageCredit = QTextBrowser()
# self.pageCredit.setOpenExternalLinks(True)
# self.pageCredit.document().setDocumentMargin(16)
self.pageLicense = QTextBrowser()
self.pageLicense.setOpenExternalLinks(True)
self.pageLicense.document().setDocumentMargin(16)
# Main Tab Area
self.tabBox = QTabWidget()
self.tabBox.addTab(self.pageAbout, "About")
# self.tabBox.addTab(self.pageCredit, "Credit")
self.tabBox.addTab(self.pageLicense, "License")
self.innerBox.addWidget(self.tabBox)
# OK Button
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
self.buttonBox.accepted.connect(self._doClose)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self._setStyleSheet()
self._fillAboutPage()
self._fillLicensePage()
logger.debug("GuiAbout initialisation complete")
return
##
# Internal Functions
##
def _fillAboutPage(self):
"""Generate the content for the About page.
"""
listPrefix = "&nbsp;&nbsp;&bull;&nbsp;&nbsp;"
aboutMsg = (
"<h2>About {name:s}</h2>"
"<p>{copyright:s}.</p>"
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
"<p>{name:s} is a markdown-like text editor designed for organising and writing "
"novels. It is written in Python 3 with a Qt5 GUI, using PyQt5.</p>"
"<p>{name:s} is free software: you can redistribute it and/or modify it under the "
"terms of the GNU General Public License as published by the Free Software Foundation, "
"either version 3 of the License, or (at your option) any later version.</p>"
"<p>{name:s} is distributed in the hope that it will be useful, but WITHOUT ANY "
"WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A "
"PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website at "
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> for more details.</p>"
"<h3>Credits</h3>"
"<p>{credits:s}</p>"
).format(
name = nw.__package__,
copyright = nw.__copyright__,
website = nw.__url__,
domain = nw.__domain__,
credits = "<br/>".join(["%s%s" % (listPrefix, x) for x in nw.__credits__]),
)
theTheme = self.theParent.theTheme
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName:
aboutMsg += (
"<h4>Theme: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theTheme.themeName,
author = theTheme.themeAuthor,
credit = theTheme.themeCredit,
license = theTheme.themeLicense,
lic_url = theTheme.themeLicenseUrl,
)
if theIcons.themeName:
aboutMsg += (
"<h4>Icons: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theIcons.themeName,
author = theIcons.themeAuthor,
credit = theIcons.themeCredit,
license = theIcons.themeLicense,
lic_url = theIcons.themeLicenseUrl,
)
if theTheme.syntaxName:
aboutMsg += (
"<h4>Syntax: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theTheme.syntaxName,
author = theTheme.syntaxAuthor,
credit = theTheme.syntaxCredit,
license = theTheme.syntaxLicense,
lic_url = theTheme.syntaxLicenseUrl,
)
self.pageAbout.setHtml(aboutMsg)
return
def _fillLicensePage(self):
"""Load the content for the License page.
"""
docName = "gplv3_%s.htm" % self.mainConf.guiLang
docPath = path.join(self.mainConf.assetPath, "text", docName)
if path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.pageLicense.setHtml(helpText)
else:
self.pageLicense.setHtml("Error loading license text ...")
return
def _setStyleSheet(self):
"""Set stylesheet for all browser tabs
"""
styleSheet = (
"h1, h2, h3, h4 {{"
" color: rgb({hColR},{hColG},{hColB});"
"}}\n"
"a {{"
" color: rgb({hColR},{hColG},{hColB});"
"}}\n"
).format(
hColR = self.theParent.theTheme.colHead[0],
hColG = self.theParent.theTheme.colHead[1],
hColB = self.theParent.theTheme.colHead[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
# self.pageCredit.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return
def _doClose(self):
self.close()
return
# END Class GuiAbout
+47 -54
View File
@@ -38,51 +38,39 @@ from PyQt5.QtWidgets import (
QFileDialog
)
from nw.gui.additions import QSwitch, QConfigLayout
from nw.gui.additions import QSwitch, QConfigLayout, PagedDialog
from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant
from nw.constants import nwAlert, nwQuotes
logger = logging.getLogger(__name__)
class GuiConfigEditor(QDialog):
class GuiConfigEditor(PagedDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
PagedDialog.__init__(self, theParent)
logger.debug("Initialising ConfigEditor ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Preferences")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings", (64,64))
self.outerBox.setSpacing(16)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
self.tabGeneral = GuiConfigEditGeneralTab(self.theParent)
self.tabLayout = GuiConfigEditLayoutTab(self.theParent)
self.tabEditing = GuiConfigEditEditingTab(self.theParent)
self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent)
self.tabWidget = QTabWidget()
self.tabWidget.setMinimumWidth(600)
self.tabWidget.addTab(self.tabGeneral, "General")
self.tabWidget.addTab(self.tabLayout, "Layout")
self.tabWidget.addTab(self.tabEditing, "Editing")
self.tabWidget.addTab(self.tabAutoRep, "Auto-Replace")
self.addTab(self.tabGeneral, "General")
self.addTab(self.tabLayout, "Layout")
self.addTab(self.tabEditing, "Editor")
self.addTab(self.tabAutoRep, "Auto-Replace")
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
self.innerBox.addWidget(self.tabWidget)
self.innerBox.addWidget(self.buttonBox)
self.addControls(self.buttonBox)
self.show()
@@ -186,22 +174,6 @@ class GuiConfigEditGeneralTab(QWidget):
"Changing this requires restarting %s." % nw.__package__
)
## Syntax Highlighting
self.selectSyntax = QComboBox()
self.selectSyntax.setMinimumWidth(200)
self.theSyntaxes = self.theTheme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes:
self.selectSyntax.addItem(syntaxName, syntaxFile)
syntaxIdx = self.selectSyntax.findData(self.mainConf.guiSyntax)
if syntaxIdx != -1:
self.selectSyntax.setCurrentIndex(syntaxIdx)
self.mainForm.addRow(
"Syntax highlight theme",
self.selectSyntax,
""
)
## Dark Icons
self.preferDarkIcons = QSwitch()
self.preferDarkIcons.setChecked(self.mainConf.guiDark)
@@ -222,13 +194,6 @@ class GuiConfigEditGeneralTab(QWidget):
self.showFullPath
)
self.highlightQuotes = QSwitch()
self.highlightQuotes.setChecked(self.mainConf.highlightQuotes)
self.mainForm.addRow(
"Add highlighting to text in quotes",
self.highlightQuotes
)
# AutoSave Settings
# =================
self.mainForm.addGroupLabel("Automatic Save")
@@ -299,11 +264,9 @@ class GuiConfigEditGeneralTab(QWidget):
needsRestart = False
guiTheme = self.selectTheme.currentData()
guiSyntax = self.selectSyntax.currentData()
guiIcons = self.selectIcons.currentData()
guiDark = self.preferDarkIcons.isChecked()
showFullPath = self.showFullPath.isChecked()
highlightQuotes = self.highlightQuotes.isChecked()
autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value()
backupPath = self.backupPath
@@ -315,11 +278,9 @@ class GuiConfigEditGeneralTab(QWidget):
needsRestart |= self.mainConf.guiIcons != guiIcons
self.mainConf.guiTheme = guiTheme
self.mainConf.guiSyntax = guiSyntax
self.mainConf.guiIcons = guiIcons
self.mainConf.guiDark = guiDark
self.mainConf.showFullPath = showFullPath
self.mainConf.highlightQuotes = highlightQuotes
self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj
self.mainConf.backupPath = backupPath
@@ -542,6 +503,33 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.setLayout(self.mainForm)
# Spell Checking
# ==============
self.mainForm.addGroupLabel("Syntax Highlighting")
## Syntax Highlighting
self.selectSyntax = QComboBox()
self.selectSyntax.setMinimumWidth(200)
self.theSyntaxes = self.theTheme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes:
self.selectSyntax.addItem(syntaxName, syntaxFile)
syntaxIdx = self.selectSyntax.findData(self.mainConf.guiSyntax)
if syntaxIdx != -1:
self.selectSyntax.setCurrentIndex(syntaxIdx)
self.mainForm.addRow(
"Highlight theme",
self.selectSyntax,
""
)
self.highlightQuotes = QSwitch()
self.highlightQuotes.setChecked(self.mainConf.highlightQuotes)
self.mainForm.addRow(
"Highlight text wrapped in quotes",
self.highlightQuotes
)
# Spell Checking
# ==============
self.mainForm.addGroupLabel("Spell Checking")
@@ -595,13 +583,17 @@ class GuiConfigEditEditingTab(QWidget):
validEntries = True
needsRestart = False
spellTool = self.spellToolList.currentData()
spellLanguage = self.spellLangList.currentData()
bigDocLimit = self.bigDocLimit.value()
guiSyntax = self.selectSyntax.currentData()
highlightQuotes = self.highlightQuotes.isChecked()
spellTool = self.spellToolList.currentData()
spellLanguage = self.spellLangList.currentData()
bigDocLimit = self.bigDocLimit.value()
self.mainConf.spellTool = spellTool
self.mainConf.spellLanguage = spellLanguage
self.mainConf.bigDocLimit = bigDocLimit
self.mainConf.guiSyntax = guiSyntax
self.mainConf.highlightQuotes = highlightQuotes
self.mainConf.spellTool = spellTool
self.mainConf.spellLanguage = spellLanguage
self.mainConf.bigDocLimit = bigDocLimit
self.mainConf.confChanged = True
@@ -782,6 +774,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
def saveValues(self):
validEntries = True
needsRestart = False
autoSelect = self.autoSelect.isChecked()
doReplace = self.autoReplaceMain.isChecked()
@@ -836,7 +829,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainConf.confChanged = True
return validEntries, False
return validEntries, needsRestart
##
# Slots
+22 -22
View File
@@ -30,10 +30,12 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton,
QListWidget, QAbstractItemView, QListWidgetItem
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox
)
from nw.constants import nwAlert, nwItemType
from nw.gui.additions import QHelpLabel
from nw.core import NWDoc
logger = logging.getLogger(__name__)
@@ -50,33 +52,31 @@ class GuiDocMerge(QDialog):
self.theProject = theProject
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.outerBox = QVBoxLayout()
self.setWindowTitle("Merge Documents")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.headLabel = QLabel("<b>Documents to Merge</b>")
self.helpLabel = QHelpLabel(
"Drag and drop items to change the order.", self.theParent.theTheme.helpText
)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox.setMinimumWidth(400)
self.listBox.setMinimumHeight(180)
self.mergeButton = QPushButton("Merge")
self.mergeButton.clicked.connect(self._doMerge)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doMerge)
self.buttonBox.rejected.connect(self._doClose)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.mergeButton, 1, 1)
self.doMergeForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addLayout(self.doMergeForm)
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
self.outerBox.addSpacing(8)
self.outerBox.addWidget(self.listBox)
self.outerBox.addSpacing(12)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
self.show()
+22 -23
View File
@@ -30,10 +30,11 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QComboBox,
QListWidget, QAbstractItemView, QListWidgetItem
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox, QLabel
)
from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout
from nw.gui.additions import QHelpLabel
from nw.core import NWDoc
logger = logging.getLogger(__name__)
@@ -51,21 +52,18 @@ class GuiDocSplit(QDialog):
self.optState = self.theProject.optState
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.outerBox = QVBoxLayout()
self.setWindowTitle("Split Document")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.headLabel = QLabel("<b>Document Headers</b>")
self.helpLabel = QHelpLabel(
"Select the maximum level to split into files.", self.theParent.theTheme.helpText
)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setMinimumWidth(400)
self.listBox.setMinimumHeight(180)
self.splitLevel = QComboBox(self)
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
@@ -79,18 +77,19 @@ class GuiDocSplit(QDialog):
self.splitLevel.setCurrentIndex(spIndex)
self.splitLevel.currentIndexChanged.connect(self._populateList)
self.splitButton = QPushButton("Split")
self.splitButton.clicked.connect(self._doSplit)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSplit)
self.buttonBox.rejected.connect(self._doClose)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 3)
self.doMergeForm.addWidget(self.splitButton, 2, 1)
self.doMergeForm.addWidget(self.closeButton, 2, 2)
self.innerBox.addLayout(self.doMergeForm)
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
self.outerBox.addSpacing(8)
self.outerBox.addWidget(self.listBox)
self.outerBox.addWidget(self.splitLevel)
self.outerBox.addSpacing(12)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
self.show()
-718
View File
@@ -1,718 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Export Tools
novelWriter GUI Export Tools
================================
Tool for exporting project files to other formats
File History:
Created: 2019-10-13 [0.2.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import time
import nw
from os import path
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout,
QGroupBox, QCheckBox, QLabel, QComboBox, QLineEdit, QPushButton,
QFileDialog, QProgressBar, QSpinBox, QMessageBox
)
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
from nw.common import packageRefURL
from nw.constants import nwFiles, nwItemType, nwAlert
logger = logging.getLogger(__name__)
class GuiExport(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiExport ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = self.theProject.optState
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Export Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64))
self.tabMain = GuiExportMain(self.theParent, self.theProject)
self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings")
self.tabWidget.addTab(self.tabPandoc, "Pandoc")
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doExportForm = QGridLayout()
self.doExportForm.setContentsMargins(10,5,0,10)
self.exportButton = QPushButton("Export")
self.exportButton.clicked.connect(self._doExport)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.exportStatus = QLabel("Ready ...")
self.exportProgress = QProgressBar(self)
self.doExportForm.addWidget(self.exportStatus, 0, 0, 1, 3)
self.doExportForm.addWidget(self.exportProgress, 1, 0)
self.doExportForm.addWidget(self.exportButton, 1, 1)
self.doExportForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addWidget(self.tabWidget)
self.innerBox.addLayout(self.doExportForm)
self.rejected.connect(self._doClose)
self.show()
logger.debug("GuiExport initialisation complete")
return
##
# Buttons
##
def _doExport(self):
logger.verbose("GuiExport export button clicked")
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
wKeywords = self.tabMain.expKeywords.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
hScene = self.tabMain.hideScene.isChecked()
hSection = self.tabMain.hideSection.isChecked()
pFormat = self.tabPandoc.outputFormat.currentData()
tFormat = GuiExportPandoc.FMT_VIA[pFormat]
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
exportDir = path.dirname(saveTo)
if not path.isdir(exportDir):
self.theParent.makeAlert("The export folder does not exist.",nwAlert.ERROR)
self.exportStatus.setText("Export failed ...")
return False
nItems = len(self.theProject.projTree)
if eFormat == GuiExportMain.FMT_PDOC:
nItems += int(0.2*nItems)
self.exportProgress.setMinimum(0)
self.exportProgress.setMaximum(nItems)
self.exportProgress.setValue(0)
if not wNovel and not wNotes:
self.exportStatus.setText("Nothing to export ...")
return False
outFile = None
if eFormat == GuiExportMain.FMT_TXT:
outFile = TextFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_MD:
outFile = MarkdownFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_HTML:
outFile = HtmlFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_TEX:
outFile = LaTeXFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_NWD:
outFile = ConcatFile(self.theProject, self.theParent)
elif eFormat == GuiExportMain.FMT_PDOC:
if tFormat == "html":
outFile = HtmlFile(self.theProject, self.theParent)
elif tFormat == "markdown":
outFile = MarkdownFile(self.theProject, self.theParent)
if outFile is None:
return False
if outFile.openFile(saveTo):
outFile.setComments(wComments)
outFile.setKeywords(wKeywords)
outFile.setExportNovel(wNovel)
outFile.setExportNotes(wNotes)
outFile.setWordWrap(fixWidth)
outFile.setChapterFormat(chFormat)
outFile.setUnNumberedFormat(unFormat)
outFile.setSceneFormat(scFormat, hScene)
outFile.setSectionFormat(seFormat, hSection)
else:
self.exportStatus.setText("Failed to open file for writing ...")
return False
time.sleep(0.5)
nDone = 0
for tItem in self.theProject.projTree:
self.exportProgress.setValue(nDone)
self.exportStatus.setText("Exporting: %s" % tItem.itemName)
logger.verbose("Exporting: %s" % tItem.itemName)
if tItem is not None and tItem.itemType == nwItemType.FILE:
outFile.addText(tItem.itemHandle)
nDone += 1
outFile.closeFile()
self.exportProgress.setValue(nDone)
self.exportStatus.setText("Export to %s complete" % outFile.fileName)
logger.verbose("Export to %s complete" % outFile.fileName)
if eFormat == GuiExportMain.FMT_TEX:
# Check that encoding was successful
if outFile.texCodecFail:
self.theParent.makeAlert((
"Failed to escape unicode characters while writing LaTeX "
"file. The generated .tex file may not build properly. "
"Make sure the python package '{package:s}' is installed "
"and working."
).format(
package = packageRefURL("latexcodec")
), nwAlert.WARN)
if eFormat != GuiExportMain.FMT_PDOC:
return True
# If we've reached this point, we're also running Pandoc
if self._callPandoc(saveTo, tFormat, pFormat):
self.exportProgress.setValue(nItems)
self.exportStatus.setText("Pandoc conversion complete")
logger.verbose("Pandoc conversion complete")
else:
self.exportProgress.setValue(nItems)
self.exportStatus.setText("Pandoc conversion failed")
logger.verbose("Pandoc conversion failed")
return False
return True
def _callPandoc(self, inFile, inFmt, outFmt):
pFmt = {
GuiExportPandoc.FMT_ODT : "odt",
GuiExportPandoc.FMT_DOCX : "docx",
GuiExportPandoc.FMT_EPUB2 : "epub2",
GuiExportPandoc.FMT_EPUB3 : "epub3",
GuiExportPandoc.FMT_ZIM : "zimwiki",
}
try:
import pypandoc
except:
self.theParent.makeAlert((
"Could not load the '{package:s}' package. "
"Make sure it is installed, and try again."
).format(
package = packageRefURL("pypandoc")
), nwAlert.ERROR)
return False
outFile = path.splitext(inFile)[0]+GuiExportPandoc.FMT_EXT[outFmt]
fileName = path.basename(outFile)
if path.isfile(outFile) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(
self.theParent, "Overwrite",
("File '%s' already exists.<br>Do you want to overwrite it?" % fileName)
)
if msgRes != QMessageBox.Yes:
return False
try:
pypandoc.convert_file(
source_file = inFile,
format = inFmt,
outputfile = outFile,
to = pFmt[outFmt],
extra_args = (),
encoding = "utf-8",
filters = None
)
except Exception as e:
self.theParent.makeAlert(
["Failed to convert file using pypandoc + Pandoc.",
str(e)], nwAlert.ERROR
)
return False
return True
def _doClose(self):
logger.verbose("GuiExport close button clicked")
# General Settings
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
wKeywords = self.tabMain.expKeywords.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
hScene = self.tabMain.hideScene.isChecked()
hSection = self.tabMain.hideSection.isChecked()
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
self.optState.setValue("GuiExport", "wNovel", wNovel)
self.optState.setValue("GuiExport", "wNotes", wNotes)
self.optState.setValue("GuiExport", "eFormat", eFormat)
self.optState.setValue("GuiExport", "fixWidth", fixWidth)
self.optState.setValue("GuiExport", "wComments", wComments)
self.optState.setValue("GuiExport", "wKeywords", wKeywords)
self.optState.setValue("GuiExport", "chFormat", chFormat)
self.optState.setValue("GuiExport", "unFormat", unFormat)
self.optState.setValue("GuiExport", "scFormat", scFormat)
self.optState.setValue("GuiExport", "seFormat", seFormat)
self.optState.setValue("GuiExport", "saveTo", saveTo)
self.optState.setValue("GuiExport", "hScene", hScene)
self.optState.setValue("GuiExport", "hSection", hSection)
# Pandoc Settings
pFormat = self.tabPandoc.outputFormat.currentData()
self.optState.setValue("GuiExport", "pFormat", pFormat)
self.optState.saveSettings()
self.close()
return
# END Class GuiExport
class GuiExportMain(QWidget):
FMT_NWD = 1 # novelWriter markdown
FMT_TXT = 2 # Plain text file
FMT_MD = 3 # Markdown file
FMT_HTML = 4 # HTML file
FMT_TEX = 5 # LaTeX file
FMT_PDOC = 6 # Pass to pandoc
FMT_EXT = {
FMT_NWD : ".nwd",
FMT_TXT : ".txt",
FMT_MD : ".md",
FMT_HTML : ".htm",
FMT_TEX : ".tex",
FMT_PDOC : ".tmp",
}
FMT_HELP = {
FMT_NWD : (
"Exports a document using the novelWriter markdown format. "
"The files selected by the filters are appended as-is, "
"including comments and other settings."
),
FMT_TXT : (
"Exports a plain text file. All formatting is stripped and "
"comments are in square brackets."
),
FMT_MD : (
"Exports a standard markdown file. Comments are converted "
"to preformatted text blocks."
),
FMT_HTML : (
"Exports a plain html5 file. Comments are wrapped in "
"blocks with a yellow background colour."
),
FMT_TEX : (
"Exports a LaTeX file that can be compiled to PDF using "
"for instance PDFLaTeX. Comments are exported as LaTeX "
"comments."
),
FMT_PDOC : (
"Exports first to markdown or html5. The file is then "
"passed on to Pandoc for a second stage. Use the Pandoc "
"tab for settings up the conversion."
),
}
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
self.optState = self.theProject.optState
self.currFormat = self.FMT_TXT
# Select Files
self.guiFiles = QGroupBox("Selection", self)
self.guiFilesForm = QGridLayout(self)
self.guiFiles.setLayout(self.guiFilesForm)
self.expNovel = QCheckBox("Novel files",self)
self.expNovel.setChecked(
self.optState.getBool("GuiExport", "wNovel", True)
)
self.expNovel.setToolTip("Include all novel files in the exported document")
self.expNotes = QCheckBox("Note files",self)
self.expNotes.setChecked(
self.optState.getBool("GuiExport", "wNotes", False)
)
self.expNotes.setToolTip("Include all note files in the exported document")
self.expComments = QCheckBox("Comments",self)
self.expComments.setChecked(
self.optState.getBool("GuiExport", "wComments", False)
)
self.expComments.setToolTip("Export comments from all files")
self.expKeywords = QCheckBox("Keywords",self)
self.expKeywords.setChecked(
self.optState.getBool("GuiExport", "wKeywords", False)
)
self.expKeywords.setToolTip("Export @keywords from all files")
self.guiFilesForm.addWidget(self.expNovel, 0, 1)
self.guiFilesForm.addWidget(self.expComments, 0, 2)
self.guiFilesForm.addWidget(self.expNotes, 1, 1)
self.guiFilesForm.addWidget(self.expKeywords, 1, 2)
self.guiFilesForm.setRowStretch(2, 1)
# Chapter Settings
self.guiChapters = QGroupBox("Chapter Headings", self)
self.guiChaptersForm = QGridLayout(self)
self.guiChapters.setLayout(self.guiChaptersForm)
self.chapterFormat = QLineEdit()
self.chapterFormat.setMaxLength(200)
self.chapterFormat.setText(
self.optState.getString("GuiExport", "chFormat", "Chapter %numword%")
)
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
self.chapterFormat.setMinimumWidth(250)
self.unnumFormat = QLineEdit()
self.unnumFormat.setMaxLength(200)
self.unnumFormat.setText(
self.optState.getString("GuiExport", "unFormat", "%title%")
)
self.unnumFormat.setToolTip("Available formats: %title%")
self.unnumFormat.setMinimumWidth(250)
self.guiChaptersForm.addWidget(QLabel("Numbered"), 0, 0)
self.guiChaptersForm.addWidget(self.chapterFormat, 0, 1)
self.guiChaptersForm.addWidget(QLabel("Unnumbered"), 1, 0)
self.guiChaptersForm.addWidget(self.unnumFormat, 1, 1)
# Scene and Section Settings
self.guiScenes = QGroupBox("Other Headings", self)
self.guiScenesForm = QGridLayout(self)
self.guiScenes.setLayout(self.guiScenesForm)
self.sceneFormat = QLineEdit()
self.sceneFormat.setMaxLength(200)
self.sceneFormat.setText(
self.optState.getString("GuiExport", "scFormat", "* * *")
)
self.sceneFormat.setToolTip("Available formats: %title%")
self.sceneFormat.setMinimumWidth(100)
self.sectionFormat = QLineEdit()
self.sectionFormat.setMaxLength(200)
self.sectionFormat.setText(
self.optState.getString("GuiExport", "seFormat", "")
)
self.sectionFormat.setToolTip("Available formats: %title%")
self.sectionFormat.setMinimumWidth(100)
self.hideScene = QCheckBox("Skip",self)
self.hideScene.setChecked(
self.optState.getBool("GuiExport", "hScene", False)
)
self.hideScene.setToolTip("Skip scene titles in export")
self.hideSection = QCheckBox("Skip",self)
self.hideSection.setChecked(
self.optState.getBool("GuiExport", "hSection", False)
)
self.hideSection.setToolTip("Skip section titles in export")
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
self.guiScenesForm.addWidget(self.sceneFormat, 0, 1)
self.guiScenesForm.addWidget(self.hideScene, 0, 2)
self.guiScenesForm.addWidget(QLabel("Sections"), 1, 0)
self.guiScenesForm.addWidget(self.sectionFormat, 1, 1)
self.guiScenesForm.addWidget(self.hideSection, 1, 2)
# Output Path
self.exportTo = QGroupBox("Export Folder", self)
self.exportToForm = QGridLayout(self)
self.exportTo.setLayout(self.exportToForm)
self.exportPath = QLineEdit(
self.optState.getString("GuiExport", "saveTo", "")
)
self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"")
self.exportGetPath.clicked.connect(self._exportFolder)
self.exportToForm.addWidget(QLabel("Save to"), 0, 0)
self.exportToForm.addWidget(self.exportPath, 0, 1)
self.exportToForm.addWidget(self.exportGetPath, 0, 2)
# Output Format
self.guiOutput = QGroupBox("Export", self)
self.guiOutputForm = QGridLayout(self)
self.guiOutput.setLayout(self.guiOutputForm)
self.outputHelp = QLabel("")
self.outputHelp.setWordWrap(True)
self.outputHelp.setMinimumHeight(55)
self.outputHelp.setAlignment(Qt.AlignTop)
self.outputFormat = QComboBox(self)
self.outputFormat.addItem("novelWriter Markdown (.nwd)", self.FMT_NWD)
self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT)
self.outputFormat.addItem("Markdown (.md)", self.FMT_MD)
self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML)
self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC)
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "eFormat", 1)
)
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
self._updateFormat(1)
else:
self.outputFormat.setCurrentIndex(optIdx)
self._updateFormat(optIdx)
self.guiOutputForm.addWidget(QLabel("Format"), 0, 0)
self.guiOutputForm.addWidget(self.outputFormat, 0, 1)
self.guiOutputForm.addWidget(self.outputHelp, 1, 0, 1, 3)
self.guiOutputForm.setColumnStretch(2, 1)
# Additional Settings
self.addSettings = QGroupBox("Additional Settings (Format Dependent)", self)
self.addSettingsForm = QGridLayout(self)
self.addSettings.setLayout(self.addSettingsForm)
self.fixedWidth = QSpinBox(self)
self.fixedWidth.setMinimum(0)
self.fixedWidth.setMaximum(999)
self.fixedWidth.setSingleStep(1)
self.fixedWidth.setValue(
self.optState.getInt("GuiExport", "fixWidth", 80)
)
self.fixedWidth.setToolTip(
"Applies to .txt and .md files. A value of '0' disables the feature."
)
self.addSettingsForm.addWidget(QLabel("Fixed width"), 0, 0)
self.addSettingsForm.addWidget(self.fixedWidth, 0, 1)
self.addSettingsForm.setColumnStretch(2, 1)
# Assemble
self.outerBox.addWidget(self.guiOutput, 0, 0, 1, 2)
self.outerBox.addWidget(self.guiFiles, 0, 2)
self.outerBox.addWidget(self.guiChapters, 1, 0, 1, 2)
self.outerBox.addWidget(self.guiScenes, 1, 2)
self.outerBox.addWidget(self.addSettings, 2, 0, 1, 3)
self.outerBox.addWidget(self.exportTo, 3, 0, 1, 3)
self.outerBox.setColumnStretch(0, 1)
self.outerBox.setColumnStretch(1, 1)
self.outerBox.setColumnStretch(2, 1)
self.setLayout(self.outerBox)
return
##
# Internal Functions
##
def _updateFormat(self, currIdx):
"""Update help text under output format selection and file
extension in file box
"""
if currIdx == -1:
self.outputHelp.setText("")
else:
self.currFormat = self.outputFormat.itemData(currIdx)
self.outputHelp.setText("<i>%s</i>" % self.FMT_HELP[self.currFormat])
self._checkFileExtension()
return
def _exportFolder(self):
currDir = self.exportPath.text()
if not path.isdir(currDir):
currDir = ""
extFilter = [
"novelWriter document files (*.nwd)",
"Text files (*.txt)",
"Markdown files (*.md)",
"HTML files (*.htm *.html)",
"LaTeX files (*.tex)",
"All files (*.*)",
]
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Export File", self.exportPath.text(),
options=dlgOpt, filter=";;".join(extFilter)
)
if saveTo:
self.exportPath.setText(saveTo[0])
self._checkFileExtension()
return True
return False
def _checkFileExtension(self):
saveTo = self.exportPath.text()
if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo)
fileBits = path.splitext(saveTo)
if self.currFormat > 0 and fileBits[0].strip() != "":
saveTo = fileBits[0]+self.FMT_EXT[self.currFormat]
self.exportPath.setText(saveTo)
return
# END Class GuiExportMain
class GuiExportPandoc(QWidget):
FMT_ODT = 1
FMT_DOCX = 2
FMT_EPUB2 = 4
FMT_EPUB3 = 5
FMT_ZIM = 6
FMT_EXT = {
FMT_ODT : ".odt",
FMT_DOCX : ".docx",
FMT_EPUB2 : ".epub",
FMT_EPUB3 : ".epub",
FMT_ZIM : ".txt",
}
FMT_VIA = {
FMT_ODT : "html",
FMT_DOCX : "html",
FMT_EPUB2 : "markdown",
FMT_EPUB3 : "markdown",
FMT_ZIM : "markdown",
}
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.outerBox = QGridLayout()
self.optState = self.theProject.optState
try:
import pypandoc
self.hasPyPan = True
except:
self.hasPyPan = False
# Information
self.guiInfo = QGroupBox("Information", self)
self.guiInfoBox = QVBoxLayout(self)
self.guiInfo.setLayout(self.guiInfoBox)
self.infoHelp = QLabel("")
self.infoHelp.setWordWrap(True)
self.infoHelp.setMinimumHeight(55)
self.infoHelp.setAlignment(Qt.AlignTop)
self.guiInfoBox.addWidget(self.infoHelp)
if self.hasPyPan:
self.infoHelp.setText((
"Additional export to other document formats than in the Settings tab is provided "
"by Pandoc. the project is first exported to Markdown or HTML, depending on final "
"format, and then processed by Pandoc into the desired format."
))
else:
self.infoHelp.setText((
"The Python package 'pypandoc' is not installed or isn't working. This package is "
"required for interfacing with Pandoc. Please install it before proceeding."
))
# Output Format
self.guiOutput = QGroupBox("Pandoc Format", self)
self.guiOutputForm = QGridLayout(self)
self.guiOutput.setLayout(self.guiOutputForm)
self.outputFormat = QComboBox(self)
self.outputFormat.addItem("Open Office Document (.odt)", self.FMT_ODT)
self.outputFormat.addItem("Word Document (.docx)", self.FMT_DOCX)
self.outputFormat.addItem("ePUB eBook v2 (.epub2)", self.FMT_EPUB2)
self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3)
self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM)
optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "pFormat", 1)
)
if optIdx == -1:
self.outputFormat.setCurrentIndex(1)
else:
self.outputFormat.setCurrentIndex(optIdx)
self.guiOutputForm.addWidget(QLabel("Format"), 0, 0)
self.guiOutputForm.addWidget(self.outputFormat, 0, 1)
self.guiOutputForm.setColumnStretch(2, 1)
# Assemble
self.outerBox.addWidget(self.guiInfo, 0, 0)
self.outerBox.addWidget(self.guiOutput, 1, 0)
self.outerBox.setRowStretch(2, 1)
self.setLayout(self.outerBox)
return
# END Class GuiExportPandoc
+56 -36
View File
@@ -30,10 +30,11 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit,
QPushButton, QComboBox, QLabel
QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel,
QDialogButtonBox
)
from nw.gui.additions import QSwitch
from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType
logger = logging.getLogger(__name__)
@@ -43,33 +44,28 @@ class GuiItemEditor(QDialog):
def __init__(self, theParent, theProject, tHandle):
QDialog.__init__(self, theParent)
logger.debug("Initialising ItemEditor ...")
logger.debug("Initialising GuiItemEditor ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theItem = self.theProject.projTree[tHandle]
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.outerBox = QVBoxLayout()
self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None:
self._doClose()
self.setWindowTitle("Item Settings")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings", (64,64))
self.outerBox.setSpacing(16)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
self.mainGroup = QGroupBox("Item Settings")
self.mainForm = QFormLayout()
# Item Label
self.editName = QLineEdit()
self.editName.setMinimumWidth(220)
self.editName.setMaxLength(200)
# Item Status
self.editStatus = QComboBox()
self.editLayout = QComboBox()
if self.theItem.itemClass == nwItemClass.NOVEL:
for sLabel, _, _ in self.theProject.statusItems:
self.editStatus.addItem(
@@ -81,6 +77,8 @@ class GuiItemEditor(QDialog):
self.theParent.importIcons[sLabel], sLabel, sLabel
)
# Item Layout
self.editLayout = QComboBox()
self.validLayouts = []
if self.theItem.itemType == nwItemType.FILE:
if self.theItem.itemClass == nwItemClass.NOVEL:
@@ -101,11 +99,15 @@ class GuiItemEditor(QDialog):
if itemLayout in self.validLayouts:
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout],itemLayout)
self.mainForm.addRow("Label", self.editName)
self.mainForm.addRow("Status", self.editStatus)
self.mainForm.addRow("Layout", self.editLayout)
self.editName.setMinimumWidth(200)
# Export Switch
self.textExport = QLabel("Include when building project")
self.editExport = QSwitch()
if self.theItem.itemType == nwItemType.FILE:
self.editExport.setEnabled(True)
self.editExport.setChecked(self.theItem.isExported)
else:
self.editExport.setEnabled(False)
self.editExport.setChecked(False)
self.editName.setText(self.theItem.itemName)
statusIdx = self.editStatus.findData(self.theItem.itemStatus)
@@ -115,44 +117,62 @@ class GuiItemEditor(QDialog):
if layoutIdx != -1:
self.editLayout.setCurrentIndex(layoutIdx)
self.buttonBox = QHBoxLayout()
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.saveButton = QPushButton("Save")
self.saveButton.setDefault(True)
self.saveButton.clicked.connect(self._doSave)
self.buttonBox.addStretch(1)
self.buttonBox.addWidget(self.closeButton)
self.buttonBox.addWidget(self.saveButton)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
self.mainGroup.setLayout(self.mainForm)
self.innerBox.addWidget(self.mainGroup)
self.innerBox.addLayout(self.buttonBox)
# Assemble
self.mainForm = QGridLayout()
self.mainForm.setVerticalSpacing(4)
self.mainForm.setHorizontalSpacing(16)
self.mainForm.addWidget(QLabel("Label"), 0, 0, 1, 1)
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
self.mainForm.addWidget(QLabel("Status"), 1, 0, 1, 1)
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
self.mainForm.addWidget(QLabel("Layout"), 2, 0, 1, 1)
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
self.show()
self.outerBox.setSpacing(16)
self.outerBox.addLayout(self.mainForm)
self.outerBox.addStretch(1)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
self.editName.selectAll()
logger.debug("ItemEditor initialisation complete")
logger.debug("GuiItemEditor initialisation complete")
return
def _doSave(self):
"""Save the setting to the item.
"""
logger.verbose("ItemEditor save button clicked")
itemName = self.editName.text()
itemStatus = self.editStatus.currentData()
itemLayout = self.editLayout.currentData()
isExported = self.editExport.isChecked()
self.theItem.setName(itemName)
self.theItem.setStatus(itemStatus)
self.theItem.setLayout(itemLayout)
self.theItem.setExported(isExported)
self.theProject.setProjectChanged(True)
self.accept()
self.close()
return
def _doClose(self):
logger.verbose("ItemEditor close button clicked")
self.reject()
self.close()
return
+34 -43
View File
@@ -31,20 +31,21 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QFormLayout, QLineEdit, QPlainTextEdit,
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit,
QLabel, QWidget, QTabWidget, QDialogButtonBox, QListWidget, QPushButton,
QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QCheckBox,
QTreeWidgetItem
)
from nw.constants import nwAlert
from nw.gui.additions import QSwitch, PagedDialog
logger = logging.getLogger(__name__)
class GuiProjectEditor(QDialog):
class GuiProjectEditor(PagedDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
PagedDialog.__init__(self, theParent)
logger.debug("Initialising ProjectEditor ...")
@@ -52,35 +53,23 @@ class GuiProjectEditor(QDialog):
self.theParent = theParent
self.theProject = theProject
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Project Settings")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings", (64,64))
self.outerBox.setSpacing(16)
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
self.theProject.countStatus()
self.setWindowTitle("Project Settings")
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject.statusItems)
self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject.importItems)
self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings")
self.tabWidget.addTab(self.tabStatus, "Status")
self.tabWidget.addTab(self.tabImport, "Importance")
self.tabWidget.addTab(self.tabReplace,"Auto-Replace")
self.addTab(self.tabMain, "Settings")
self.addTab(self.tabStatus, "Status")
self.addTab(self.tabImport, "Importance")
self.addTab(self.tabReplace,"Auto-Replace")
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
self.innerBox.addWidget(self.tabWidget)
self.innerBox.addWidget(self.buttonBox)
self.addControls(self.buttonBox)
self.show()
@@ -94,7 +83,7 @@ class GuiProjectEditor(QDialog):
projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text()
bookAuthors = self.tabMain.editAuthors.toPlainText()
doBackup = self.tabMain.doBackup.isChecked()
doBackup = not self.tabMain.doBackup.isChecked()
self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors)
@@ -130,37 +119,39 @@ class GuiProjectEditMain(QWidget):
self.theParent = theParent
self.theProject = theProject
self.mainForm = QFormLayout()
self.mainForm = QGridLayout()
self.backupBox = QHBoxLayout()
self.editName = QLineEdit()
self.editTitle = QLineEdit()
self.editAuthors = QPlainTextEdit()
self.doBackup = QCheckBox(self)
self.editName = QLineEdit()
self.editName.setMaxLength(200)
self.editTitle.setMaxLength(200)
self.mainForm.addRow("Working Title", self.editName)
self.mainForm.addRow("Book Title", self.editTitle)
self.mainForm.addRow("Book Authors", self.editAuthors)
self.mainForm.addRow(self.backupBox)
self.backupBox.addStretch(1)
self.backupBox.addWidget(QLabel("Backup on Close"))
self.backupBox.addWidget(self.doBackup)
self.editName.setText(self.theProject.projName)
self.editTitle = QLineEdit()
self.editTitle.setMaxLength(200)
self.editTitle.setText(self.theProject.bookTitle)
self.editAuthors = QPlainTextEdit()
bookAuthors = ""
for bookAuthor in self.theProject.bookAuthors:
bookAuthors += bookAuthor+"\n"
self.editAuthors.setPlainText(bookAuthors)
if self.theProject.doBackup:
self.doBackup.setCheckState(Qt.Checked)
else:
self.doBackup.setCheckState(Qt.Unchecked)
self.editAuthors.setMaximumHeight(120)
self.doBackup = QSwitch(self)
self.doBackup.setChecked(not self.theProject.doBackup)
self.backupBox.addStretch(1)
self.backupBox.addWidget(QLabel("Disable backup on close"))
self.backupBox.addWidget(self.doBackup)
self.mainForm.addWidget(QLabel("Working title"), 0, 0, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(self.editName, 0, 1, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(QLabel("Book title"), 1, 0, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(self.editTitle, 1, 1, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(QLabel("Book authors"), 2, 0, 1, 1, Qt.AlignTop)
self.mainForm.addWidget(self.editAuthors, 2, 1, 1, 1, Qt.AlignTop)
self.mainForm.addLayout(self.backupBox, 3, 0, 1, 2, Qt.AlignTop)
self.setLayout(self.mainForm)
self.editAuthors.setMaximumHeight(120)
return
+68 -29
View File
@@ -33,7 +33,7 @@ from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem
QAbstractItemView, QTreeWidgetItem, QDialogButtonBox, QLabel
)
from nw.common import formatInt
@@ -42,25 +42,33 @@ logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
NONE_STATE = 0
BROWSE_STATE = 1
NEW_STATE = 2
OPEN_STATE = 3
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectLoad ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
self.openPath = None
self.mainConf = nw.CONFIG
self.theParent = theParent
self.openState = self.NONE_STATE
self.openPath = None
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.outerBox.setSpacing(16)
self.innerBox.setSpacing(16)
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Open Project")
self.setLayout(self.outerBox)
self.setMinimumWidth(650)
self.setMinimumHeight(400)
self.setModal(True)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (96, 96))
self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.projectForm = QGridLayout()
self.projectForm.setContentsMargins(0, 0, 0, 0)
@@ -71,34 +79,45 @@ class GuiProjectLoad(QDialog):
self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels(["Working Title","Words","Last Opened","Path"])
self.listBox.setRootIsDecorated(False)
self.listBox.setColumnHidden(3, True)
self.listBox.itemClicked.connect(self._doSelectRecent)
self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
treeHead = self.listBox.headerItem()
treeHead.setTextAlignment(1, Qt.AlignRight)
treeHead.setTextAlignment(2, Qt.AlignRight)
self.recentButton = QPushButton("Open")
self.recentButton.clicked.connect(self._doOpenRecent)
self.browseButton = QPushButton("Browse")
self.browseButton.clicked.connect(self._doBrowse)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.lblRecent = QLabel("<b>Recently Opened:</b>")
self.lblPath = QLabel("<b>Path:</b>")
self.selPath = QLabel("")
self.selPath.setWordWrap(True)
self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
self.projectForm.addWidget(self.recentButton, 1, 1)
self.projectForm.addWidget(self.browseButton, 1, 2)
self.projectForm.addWidget(self.closeButton, 1, 3)
self.projectForm.setColumnStretch(0, 1)
self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 2)
self.projectForm.addWidget(self.listBox, 1, 0, 1, 2)
self.projectForm.addWidget(self.lblPath, 2, 0, 1, 1, Qt.AlignTop)
self.projectForm.addWidget(self.selPath, 2, 1, 1, 1, Qt.AlignTop)
self.projectForm.setColumnStretch(1, 1)
self.projectForm.setVerticalSpacing(4)
self.projectForm.setHorizontalSpacing(8)
self.innerBox.addLayout(self.projectForm)
self.rejected.connect(self._doClose)
self.setModal(True)
self.setMinimumWidth(750)
self.setMinimumHeight(450)
self.show()
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doOpenRecent)
self.buttonBox.rejected.connect(self._doClose)
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
self.newButton.clicked.connect(self._doNewProject)
self.browseButton = self.buttonBox.addButton("Browse", QDialogButtonBox.ActionRole)
self.browseButton.clicked.connect(self._doBrowse)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self._populateList()
self._doSelectRecent()
logger.debug("GuiProjectLoad initialisation complete")
@@ -116,10 +135,19 @@ class GuiProjectLoad(QDialog):
selItems = self.listBox.selectedItems()
if selItems:
self.openPath = selItems[0].text(3)
self.openState = self.OPEN_STATE
self.accept()
else:
self.openPath = None
self.openState = self.NONE_STATE
return
def _doSelectRecent(self):
"""A recent item has been selected.
"""
selList = self.listBox.selectedItems()
if selList:
self.selPath.setText(selList[0].text(3))
return
def _doBrowse(self):
@@ -129,6 +157,7 @@ class GuiProjectLoad(QDialog):
logger.verbose("GuiProjectLoad browse button clicked")
self._saveDialogState()
self.openPath = None
self.openState = self.BROWSE_STATE
self.accept()
return
@@ -140,6 +169,16 @@ class GuiProjectLoad(QDialog):
self.close()
return
def _doNewProject(self):
"""Create a new project.
"""
logger.verbose("GuiProjectLoad new project button clicked")
self._saveDialogState()
self.openPath = None
self.openState = self.NEW_STATE
self.accept()
return
##
# Internal Functions
##
@@ -156,7 +195,6 @@ class GuiProjectLoad(QDialog):
def _populateList(self):
"""Populate the list box with recent project data.
"""
listOrder = []
listData = {}
for projPath in self.mainConf.recentProj.keys():
@@ -178,6 +216,7 @@ class GuiProjectLoad(QDialog):
hasSelection = False
for timeStamp in sorted(listOrder, reverse=True):
newItem = QTreeWidgetItem([""]*4)
newItem.setIcon(0, self.theParent.theTheme.getIcon("proj_nwx"))
newItem.setText(0, listData[timeStamp][0])
newItem.setText(1, formatInt(listData[timeStamp][1]))
newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
+168 -33
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel
from nw.constants import nwLabels
from nw.constants import nwLabels, nwItemClass, nwItemType, nwUnicode
logger = logging.getLogger(__name__)
@@ -48,58 +48,193 @@ class GuiDocDetails(QFrame):
self.mainBox = QGridLayout(self)
self.mainBox.setVerticalSpacing(1)
self.mainBox.setHorizontalSpacing(15)
self.mainBox.setHorizontalSpacing(6)
self.setLayout(self.mainBox)
self.fntOne = QFont()
self.fntOne.setPointSize(10)
self.fntOne.setBold(True)
self.fntLabel = QFont()
self.fntLabel.setPointSize(10)
self.fntLabel.setBold(True)
self.fntTwo = QFont()
self.fntTwo.setPointSize(10)
self.colTwo = [
QLabel(""),
QLabel(""),
QLabel(""),
QLabel("")
]
self.fntFixed = QFont()
self.fntFixed.setFamily("Monospace")
self.fntFixed.setPointSize(10)
colOne = ["Label","Status","Class","Layout"]
for nRow in range(4):
lblOne = QLabel(colOne[nRow])
lblOne.setFont(self.fntOne)
lblOne.setAlignment(Qt.AlignTop)
self.mainBox.addWidget(lblOne,nRow,0)
self.mainBox.addWidget(self.colTwo[nRow],nRow,1)
self.colTwo[nRow].setWordWrap(True)
self.colTwo[nRow].setAlignment(Qt.AlignTop)
self.fntValue = QFont()
self.fntValue.setPointSize(10)
# Label
self.labelName = QLabel("Label ")
self.labelName.setFont(self.fntLabel)
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelFlag = QLabel("")
self.labelFlag.setFont(self.fntFixed)
self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline)
self.labelData = QLabel("")
self.labelData.setFont(self.fntValue)
self.labelData.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelData.setWordWrap(True)
# Status
self.statusName = QLabel("Status ")
self.statusName.setFont(self.fntLabel)
self.statusName.setAlignment(Qt.AlignLeft)
self.statusFlag = QLabel("")
self.statusFlag.setFont(self.fntFixed)
self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.statusData = QLabel("")
self.statusData.setFont(self.fntValue)
self.statusData.setAlignment(Qt.AlignLeft)
# Class
self.className = QLabel("Class ")
self.className.setFont(self.fntLabel)
self.className.setAlignment(Qt.AlignLeft)
self.classFlag = QLabel("")
self.classFlag.setFont(self.fntFixed)
self.classFlag.setAlignment(Qt.AlignRight)
self.classData = QLabel("")
self.classData.setFont(self.fntValue)
self.classData.setAlignment(Qt.AlignLeft)
# Layout
self.layoutName = QLabel("Layout ")
self.layoutName.setFont(self.fntLabel)
self.layoutName.setAlignment(Qt.AlignLeft)
self.layoutFlag = QLabel("")
self.layoutFlag.setFont(self.fntFixed)
self.layoutFlag.setAlignment(Qt.AlignRight)
self.layoutData = QLabel("")
self.layoutData.setFont(self.fntValue)
self.layoutData.setAlignment(Qt.AlignLeft)
# Character Count
self.cCountName = QLabel(" Characters")
self.cCountName.setFont(self.fntLabel)
self.cCountName.setAlignment(Qt.AlignRight)
self.cCountData = QLabel("")
self.cCountData.setFont(self.fntValue)
self.cCountData.setAlignment(Qt.AlignRight)
# Word Count
self.wCountName = QLabel(" Words")
self.wCountName.setFont(self.fntLabel)
self.wCountName.setAlignment(Qt.AlignRight)
self.wCountData = QLabel("")
self.wCountData.setFont(self.fntValue)
self.wCountData.setAlignment(Qt.AlignRight)
# Paragraph Count
self.pCountName = QLabel(" Paragraphs")
self.pCountName.setFont(self.fntLabel)
self.pCountName.setAlignment(Qt.AlignRight)
self.pCountData = QLabel("")
self.pCountData.setFont(self.fntValue)
self.pCountData.setAlignment(Qt.AlignRight)
# Assemble
self.mainBox.addWidget(self.labelName, 0, 0, 1, 1)
self.mainBox.addWidget(self.labelFlag, 0, 1, 1, 1)
self.mainBox.addWidget(self.labelData, 0, 2, 1, 3)
self.mainBox.addWidget(self.statusName, 1, 0, 1, 1)
self.mainBox.addWidget(self.statusFlag, 1, 1, 1, 1)
self.mainBox.addWidget(self.statusData, 1, 2, 1, 1)
self.mainBox.addWidget(self.cCountName, 1, 3, 1, 1)
self.mainBox.addWidget(self.cCountData, 1, 4, 1, 1)
self.mainBox.addWidget(self.className, 2, 0, 1, 1)
self.mainBox.addWidget(self.classFlag, 2, 1, 1, 1)
self.mainBox.addWidget(self.classData, 2, 2, 1, 1)
self.mainBox.addWidget(self.wCountName, 2, 3, 1, 1)
self.mainBox.addWidget(self.wCountData, 2, 4, 1, 1)
self.mainBox.addWidget(self.layoutName, 3, 0, 1, 1)
self.mainBox.addWidget(self.layoutFlag, 3, 1, 1, 1)
self.mainBox.addWidget(self.layoutData, 3, 2, 1, 1)
self.mainBox.addWidget(self.pCountName, 3, 3, 1, 1)
self.mainBox.addWidget(self.pCountData, 3, 4, 1, 1)
self.mainBox.setColumnStretch(0,0)
self.mainBox.setColumnStretch(1,1)
self.mainBox.setColumnStretch(1,0)
self.mainBox.setColumnStretch(2,1)
logger.debug("DocDetails initialisation complete")
return
def buildViewBox(self, tHandle):
###
# Class Methods
##
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
colTwo = [""]*4
self.labelFlag.setText("")
self.labelData.setText("")
self.statusFlag.setText("")
self.statusData.setText("")
self.classFlag.setText("")
self.classData.setText("")
self.layoutFlag.setText("")
self.layoutData.setText("")
self.cCountData.setText("")
self.wCountData.setText("")
self.pCountData.setText("")
else:
theLabel = nwItem.itemName
if len(theLabel) > 100:
theLabel = theLabel[:96].rstrip()+" ..."
colTwo = [
theLabel,
nwItem.itemStatus,
nwLabels.CLASS_NAME[nwItem.itemClass],
nwLabels.LAYOUT_NAME[nwItem.itemLayout],
]
for nRow in range(4):
self.colTwo[nRow].setText(colTwo[nRow])
iStatus = nwItem.itemStatus
if nwItem.itemClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[iStatus]
else:
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[iStatus]
if nwItem.itemType == nwItemType.FILE:
if nwItem.isExported:
exportFlag = nwUnicode.U_CHECK
else:
exportFlag = " "
else:
exportFlag = "-"
self.labelFlag.setText(exportFlag)
self.statusFlag.setPixmap(flagIcon.pixmap(10, 10))
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
self.labelData.setText(theLabel)
self.statusData.setText(nwItem.itemStatus)
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
if nwItem.itemType == nwItemType.FILE:
self.cCountData.setText("{:n}".format(nwItem.charCount))
self.wCountData.setText("{:n}".format(nwItem.wordCount))
self.pCountData.setText("{:n}".format(nwItem.paraCount))
else:
self.cCountData.setText("")
self.wCountData.setText("")
self.pCountData.setText("")
return
+9 -1
View File
@@ -32,7 +32,7 @@ from time import time
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtWidgets import (
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QLabel
qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox
)
from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -363,6 +363,14 @@ class GuiDocEditor(QTextEdit):
return
def updateDocTitle(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self.theHandle:
self.docTitle.setTitleFromHandle(self.theHandle)
return
##
# Setters and Getters
##
+19 -6
View File
@@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
from nw.core import NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwUnicode
)
logger = logging.getLogger(__name__)
@@ -254,6 +254,8 @@ class GuiDocTree(QTreeWidget):
return theList
def getColumnSizes(self):
"""Return the column widths for the tree columns.
"""
retVals = [
self.columnWidth(0),
self.columnWidth(1),
@@ -401,7 +403,8 @@ class GuiDocTree(QTreeWidget):
return True
def setTreeItemValues(self, tHandle):
"""Set the name and flag values for a tree item.
"""
trItem = self._getTreeItem(tHandle)
nwItem = self.theProject.projTree[tHandle]
tName = nwItem.itemName
@@ -409,9 +412,19 @@ class GuiDocTree(QTreeWidget):
tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle
tStatus = nwLabels.CLASS_FLAG[nwItem.itemClass]
stExport = " "
stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
stLayout = ""
if nwItem.itemType == nwItemType.FILE:
tStatus += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
if nwItem.isExported:
stExport = nwUnicode.U_CHECK
else:
stExport = "-"
tStatus = stExport+" "+stClass+stLayout
iStatus = nwItem.itemStatus
if tClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
@@ -525,8 +538,8 @@ class GuiDocTree(QTreeWidget):
newItem.setText(self.C_HANDLE, tHandle)
# newItem.setForeground(self.C_COUNT,QColor(*self.theParent.theTheme.treeWCount))
newItem.setTextAlignment(self.C_COUNT,Qt.AlignRight)
newItem.setFont(self.C_FLAGS,self.fontFlags)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setFont(self.C_FLAGS, self.fontFlags)
self.theMap[tHandle] = newItem
if pHandle is None:
+9 -1
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
from nw.convert import ToHtml
from nw.core import ToHtml
from nw.constants import nwAlert, nwItemType, nwDocAction
from nw.gui.elements.doctitlebar import GuiDocTitleBar
@@ -192,6 +192,14 @@ class GuiDocViewer(QTextBrowser):
return False
return True
def updateDocTitle(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self.theHandle:
self.docTitle.setTitleFromHandle(self.theHandle)
return
##
# Events
##
+6 -7
View File
@@ -76,6 +76,7 @@ class GuiIcons:
"cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
"proj_folder" : (QStyle.SP_DirIcon, "folder"),
"proj_nwx" : (None, None),
"status_lang" : (None, None),
"status_time" : (None, None),
"status_stats" : (None, None),
@@ -98,11 +99,7 @@ class GuiIcons:
}
DECO_MAP = {
"nwicon" : ["icons", "novelWriter.svg"],
"export" : ["graphics", "export.svg"],
"merge" : ["graphics", "merge.svg"],
"settings" : ["graphics", "gear.svg"],
"split" : ["graphics", "split.svg"],
"nwicon" : ["icons", "novelwriter.svg"],
}
def __init__(self, theParent):
@@ -299,8 +296,10 @@ class GuiIcons:
# Finally. we check if we have a fallback icon
if self.mainConf.guiDark:
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey)
else:
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
if path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
return QIcon(fbackIcon)
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
if path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme" % iconKey)
return QIcon(fbackIcon)
+12 -54
View File
@@ -32,6 +32,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox
from nw.gui.dialogs import GuiAbout
from nw.constants import nwItemType, nwItemClass, nwDocAction
logger = logging.getLogger(__name__)
@@ -119,54 +120,11 @@ class GuiMainMenu(QMenuBar):
return True
def _showAbout(self):
listPrefix = "&nbsp;&nbsp;&bull;&nbsp;&nbsp;"
aboutMsg = (
"<h3>About {name:s}</h3>"
"<p>Version {version:s}, released {date:s}.</p>"
"<p>{copyright:s}.</p>"
"<p>{name:s} is a markdown-like text editor designed for organising "
"and writing novels. It is written in Python 3 with a Qt5 GUI, "
"using PyQt5.</p>"
"<p>{name:s} is free software: you can redistribute it and/or "
"modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of the "
"License, or (at your option) any later version.</p>"
"<p>{name:s} is distributed in the hope that it will be useful, "
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p><p>See the "
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"License for more details.</p>"
"<p>Website: <a href='{website:s}'>{website:s}</a></p>"
"<h4>Credits</h4>"
"<p>{credits:s}</p>"
).format(
name = nw.__package__,
version = nw.__version__,
date = nw.__date__,
copyright = nw.__copyright__,
website = nw.__url__,
credits = "<br/>".join(["%s%s" % (listPrefix, x) for x in nw.__credits__]),
)
theTheme = self.theParent.theTheme
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeCredit != "" or theTheme.syntaxCredit != "":
aboutMsg += "<h4>GUI Theme and Syntax Highlighting</h4>"
aboutMsg += "<p>"
if theTheme.themeCredit != "":
aboutMsg += "%s\"%s\" by %s<br/>" % (
listPrefix, theTheme.themeName, theTheme.themeCredit
)
if theIcons.themeCredit != "":
aboutMsg += "%s\"%s\" by %s<br/>" % (
listPrefix, theIcons.themeName, theIcons.themeCredit
)
if theTheme.syntaxCredit != "":
aboutMsg += "%s\"%s\" by %s<br/>" % (
listPrefix, theTheme.syntaxName, theTheme.syntaxCredit
)
aboutMsg += "</p>"
msgBox = QMessageBox()
msgBox.about(self.theParent, "About %s" % nw.__package__, aboutMsg)
"""Show the about dialog.
"""
if self.mainConf.showGUI:
msgAbout = GuiAbout(self.theParent)
msgAbout.exec_()
return True
def _showAboutQt(self):
@@ -230,11 +188,11 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addAction(self.aProjectSettings)
# Project > Export Project
self.aExportProject = QAction("Export Project", self)
self.aExportProject.setStatusTip("Export project")
self.aExportProject.setShortcut("F5")
self.aExportProject.triggered.connect(self.theParent.exportProjectDialog)
self.projMenu.addAction(self.aExportProject)
self.aBuildProject = QAction("Build Project", self)
self.aBuildProject.setStatusTip("Build project")
self.aBuildProject.setShortcut("F5")
self.aBuildProject.triggered.connect(self.theParent.buildProjectDialog)
self.projMenu.addAction(self.aBuildProject)
# Project > Session Log
self.aSessionLog = QAction("Session Log", self)
@@ -357,7 +315,7 @@ class GuiMainMenu(QMenuBar):
self.docuMenu.addAction(self.aCloseView)
# Document > Toggle View Comments
self.aViewDocComments = QAction("View Comments", self)
self.aViewDocComments = QAction("Show Comments", self)
self.aViewDocComments.setStatusTip("Show comments in view panel")
self.aViewDocComments.setCheckable(True)
self.aViewDocComments.setChecked(self.mainConf.viewComments)

Some files were not shown because too many files have changed in this diff Show More