Merge branch 'dev' into new_ref_panel

This commit is contained in:
Veronica Berglyd Olsen
2023-11-14 11:06:04 +01:00
84 changed files with 673 additions and 1761 deletions
+5 -23
View File
@@ -33,26 +33,8 @@ from novelwriter.error import exceptionHandler, logException
from novelwriter.config import Config
from novelwriter.shared import SharedData
##
# Version Scheme
# ================
# Generally follows PEP 440
# Hex Version:
# - Digit 1,2 : Major Version (01-ff)
# - Digit 3,4 : Minor Version (01-ff)
# - Digit 5,6 : Patch Version (01-ff)
# - Digit 7 : Release Type (a: alpha, b: beta, c: candidate, f: final)
# - Digit 8 : Release Number (0-f)
#
# Example : Full Short Description
# --------------------------------------------------------------------
# 0x010200a0 : 1.2-alpha0 1.2a0 Use while developing next release
# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release
# 0x010200b1 : 1.2-beta1 1.2b1 First beta release
# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate
# 0x010200f0 : 1.2.0 1.2.0 Final release
# 0x010201f0 : 1.2.1 1.2.1 Patch release
##
# Package Meta
# ============
__package__ = "novelwriter"
__copyright__ = "Copyright 20182023, Veronica Berglyd Olsen"
@@ -60,9 +42,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.2-alpha1"
__hexversion__ = "0x020200a1"
__date__ = "2023-09-01"
__version__ = "2.2-beta1"
__hexversion__ = "0x020200b1"
__date__ = "2023-11-12"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -2,6 +2,16 @@
"Synopsis": "Samandrag",
"Comment": "Kommentar",
"Notes": "Notat",
"Tag": "Knagg",
"Point of View": "Perspektiv",
"Focus": "Fokus",
"Characters": "Karakterar",
"Plot": "Plott",
"Timeline": "Tidslinje",
"Locations": "Lokasjonar",
"Objects": "Objekt",
"Entities": "Eining",
"Custom": "Anna",
"0": "null",
"1": "ein",
"2": "to",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 167 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 167 KiB

@@ -1,207 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 270.93333 270.93334"
height="1024"
width="1024">
<defs
id="defs2">
<linearGradient
id="linearGradient2515">
<stop
style="stop-color:#f7f7f7;stop-opacity:1"
offset="0"
id="stop2511" />
<stop
style="stop-color:#e5e5e5;stop-opacity:1"
offset="1"
id="stop2513" />
</linearGradient>
<rect
x="60.484347"
y="205.79962"
width="130.57787"
height="65.785888"
id="rect852" />
<rect
id="rect1474"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<rect
id="rect852-7"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1544"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1570" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1587" />
<filter
id="filter1444"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1434"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1436"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1438"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1440"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1442"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<filter
id="filter1456"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1446"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1448"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1450"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1452"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1454"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<radialGradient
xlink:href="#linearGradient2515"
id="radialGradient2517"
cx="605.42725"
cy="623.97186"
fx="605.42725"
fy="623.97186"
r="410"
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer3"
style="display:inline">
<path
clip-path="none"
transform="scale(0.26458333)"
d="m 192,22 c -44.32,0 -80,35.680002 -80,80 v 820 c 0,44.32002 35.68,80 80,80 h 640 c 44.31999,0 80,-35.67998 80,-80 V 213.44727 L 720.55469,22 Z"
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1481" />
<path
id="path1490"
style="display:inline;fill:#e0e0e0;fill-opacity:1;stroke:#999999;stroke-width:5.29167;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 241.30037,56.474215 190.64713,5.8204593 V 35.307549 c 0,11.726333 9.44034,21.166666 21.16667,21.166666 z" />
<g
aria-label=".nwx"
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
id="text850"
style="font-size:5.64444px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect852);display:inline;fill:#666666;fill-opacity:1">
<path
d="m 63.088865,241.76928 q 0,-1.36426 0.909505,-2.27376 0.930175,-0.93018 2.480467,-0.93018 1.550291,0 2.459796,0.93018 0.909504,0.9095 0.909504,2.27376 0,1.34359 -0.909504,2.27376 -0.909505,0.9095 -2.459796,0.9095 -1.550292,0 -2.480467,-0.9095 -0.909505,-0.93017 -0.909505,-2.27376 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path856" />
<path
d="m 84.75161,226.78312 q -2.542479,0 -3.782712,2.14974 v 15.79231 h -5.973791 v -22.36554 h 5.601721 l 0.206705,2.56314 q 2.397785,-2.97656 6.428544,-2.97656 3.162595,0 5.105627,1.88103 1.943033,1.88102 1.963703,6.55256 v 14.34537 h -5.994461 v -14.26269 q 0,-2.14973 -0.930175,-2.91454 -0.909505,-0.76482 -2.625161,-0.76482 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path858" />
<path
d="m 126.96089,222.35963 -5.68441,22.36554 h -5.00227 l -4.25814,-14.07665 -4.23746,14.07665 h -4.9816 l -5.705078,-22.36554 h 5.767088 l 2.95589,14.22134 4.05143,-14.22134 h 4.32014 l 4.09277,14.20067 2.91455,-14.20067 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path860" />
<path
d="m 134.77436,222.35963 3.65869,6.65592 3.76204,-6.65592 h 6.3872 l -6.36653,10.93472 6.63525,11.43082 h -6.3872 l -4.01009,-7.04866 -3.94808,7.04866 h -6.42854 l 6.63525,-11.43082 -6.36654,-10.93472 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path862" />
</g>
</g>
<g
id="g2509"
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)">
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="rect1406-1"
style="display:inline;fill:#a11212;fill-opacity:1;stroke:#7e1b1b;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1444)"
d="m 145.18984,56.414257 c -44.32,0 -79.999998,35.68 -79.999998,80.000003 v 199.99805 a 279.99999,279.99999 0 0 0 0,0.002 279.99999,279.99999 0 0 0 0,0.0195 v 199.98242 c 0,44.31999 35.679998,80 79.999998,80 44.32,0 80,-35.68001 80,-80 v -200 a 120,120 0 0 1 120,-120 120,120 0 0 1 120,120 v 282.83592 h 160 V 336.41621 a 279.99999,279.99999 0 0 0 -280,-280.000003 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 c -14.4245,-20.75262 -38.42829,-34.29883 -65.72461,-34.29883 z M 376.65078,619.64082 c -0.62174,0.0972 -1.2172,0.22317 -1.7793,0.37696 0.5621,-0.15379 1.15756,-0.27973 1.7793,-0.37696 z m -1.7793,0.37696 c -0.5621,0.15378 -1.09071,0.33534 -1.58008,0.53906 0.48937,-0.20372 1.01798,-0.38528 1.58008,-0.53906 z m 338.85742,-0.37696 c 0.62174,0.0972 1.2172,0.22317 1.7793,0.37696 -0.5621,-0.15379 -1.15756,-0.27973 -1.7793,-0.37696 z m 1.7793,0.37696 c 0.5621,0.15378 1.09071,0.33534 1.58008,0.53906 -0.48938,-0.20372 -1.01798,-0.38528 -1.58008,-0.53906 z" />
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="path897"
style="display:inline;fill:#730d0d;fill-opacity:1;stroke:#590d0d;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1456)"
d="m 465.18984,619.25215 v 0.16406 h -85.58398 c -5.49302,0 -9.91602,2.2329 -9.91602,5.00586 0,0.52294 0.15913,1.02696 0.45117,1.5 h -0.44531 c 0.30916,2.03607 1.24102,3.99791 2.81641,5.57227 l 165.59961,165.49414 c 3.92061,3.91808 10.23371,3.91812 14.15429,0 L 717.86757,631.49434 c 1.57569,-1.57463 2.50725,-3.53578 2.81641,-5.57227 h -0.44531 c 0.29216,-0.47304 0.45117,-0.97706 0.45117,-1.5 0,-2.77296 -4.42304,-5.00586 -9.91602,-5.00586 h -85.58398 v -0.16406 z" />
<rect
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1"
id="rect914"
width="6.7583966"
height="82.482399"
x="-118.27303"
y="1.2969339"
rx="3.3791983"
ry="3.6658845" />
</g>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="256" height="256" version="1.1" viewBox="0 0 67.733 67.733" xmlns="http://www.w3.org/2000/svg">
<path d="m12.869 1.7145c-2.9081 0-5.2493 2.3412-5.2493 5.2493v53.806c0 2.9081 2.3412 5.2493 5.2493 5.2493h41.995c2.9081 0 5.2493-2.3412 5.2493-5.2493v-46.493l-12.562-12.562z" fill="#ececec" stroke="#999" stroke-linejoin="round" stroke-width="1.3123"/>
<path d="m60.113 14.277-12.562-12.562v7.3128c0 2.9081 2.3412 5.2493 5.2493 5.2493z" fill="#e0e0e0" stroke="#999" stroke-linejoin="round" stroke-width="1.3123"/>
<path d="m34.617 50.796c-0.31888 0-0.57319 0.09267-0.7628 0.27797-0.1853 0.18099-0.27797 0.41156-0.27797 0.69167 0 0.2758 0.09268 0.50636 0.27797 0.69166 0.18961 0.18099 0.44392 0.27144 0.7628 0.27144s0.57306-0.09044 0.76266-0.27144c0.18961-0.1853 0.28438-0.41587 0.28438-0.69166 0-0.28011-0.09477-0.51067-0.28438-0.69167-0.18961-0.1853-0.44378-0.27797-0.76266-0.27797zm-21.512 0.36204v9.4116h1.9456v-3.3226h1.7323c1.1118 0 1.978-0.26924 2.5985-0.8079 0.62484-0.53867 0.93722-1.2627 0.93722-2.172 0-0.90065-0.31238-1.644-0.93722-2.2301-0.62053-0.58607-1.4867-0.87903-2.5985-0.87903zm38.395 0.69154v1.7259h-1.0278v1.364h1.0278v3.6909c0 0.74121 0.1809 1.2713 0.54287 1.5902 0.36198 0.31889 0.84896 0.47828 1.4609 0.47828 0.40076 0 0.77566-0.0582 1.1247-0.17455v-1.4156c-0.17237 0.03448-0.3534 0.05178-0.543 0.05178-0.23701 0-0.41581-0.04745-0.53647-0.14226-0.12066-0.09481-0.18096-0.28867-0.18096-0.58171v-3.497h1.1893v-1.364h-1.1893v-1.7259zm-36.45 0.87916h1.7323c0.55589 0 0.95662 0.15729 1.2022 0.47188 0.24562 0.31027 0.36845 0.67008 0.36845 1.0795 0 0.40939-0.12283 0.7456-0.36845 1.0085-0.24562 0.26287-0.64636 0.39421-1.2022 0.39421h-1.7323zm9.8574 0.71755c-0.67224 0-1.1915 0.32101-1.5578 0.96311l-0.05818-0.83392h-1.7646v6.9942h1.8744v-4.583c0.21977-0.47834 0.68093-0.71756 1.3833-0.71756l0.67872 0.05178 0.01934-1.7906c-0.14651-0.05602-0.33829-0.08395-0.5753-0.08395zm4.2402 0c-1.0514 0-1.8659 0.34041-2.4433 1.0213-0.57743 0.67657-0.86609 1.5233-0.86609 2.5403v0.21979c0.01724 0.99546 0.31244 1.8228 0.88557 2.4822 0.57743 0.65933 1.3897 0.98899 2.4368 0.98899 1.0558 0 1.8702-0.33831 2.4433-1.0149 0.57312-0.67657 0.85968-1.5233 0.85968-2.5403v-0.13572c0-1.017-0.28655-1.8638-0.85968-2.5403-0.57313-0.68088-1.3919-1.0213-2.4563-1.0213zm10.93 0c-0.99974 0-1.7948 0.33819-2.3851 1.0148-0.58605 0.67657-0.87903 1.5429-0.87903 2.5987v0.27144c0.0043 0.93082 0.31236 1.7259 0.92427 2.3853 0.61191 0.65502 1.4608 0.98246 2.5467 0.98246 0.68085 0 1.2498-0.13134 1.7065-0.39421 0.45678-0.26287 0.79721-0.55386 1.0213-0.87275l-0.9244-1.0213c-0.41368 0.52143-0.97605 0.78214-1.6871 0.78214-0.47832 0-0.86398-0.13789-1.157-0.41369-0.29303-0.28011-0.47183-0.64634-0.53647-1.0988h4.4536v-0.79509c0-1.0342-0.26296-1.8659-0.78868-2.4951-0.52572-0.62917-1.2906-0.94376-2.2947-0.94376zm7.0715 0c-1.0816 0-1.8896 0.34251-2.424 1.0277-0.53434 0.68519-0.8015 1.5212-0.8015 2.508v0.18108c0 0.99115 0.26716 1.8293 0.8015 2.5145 0.53865 0.68088 1.3488 1.0213 2.4304 1.0213 0.81013 0 1.4932-0.23909 2.0491-0.71743 0.5602-0.47834 0.84453-1.0881 0.85314-1.8293h-1.7581c-0.0043 0.31889-0.11639 0.57306-0.33616 0.76267-0.21977 0.1853-0.4999 0.27797-0.84033 0.27797-0.55158 0-0.91141-0.20251-1.0795-0.6076-0.16375-0.40939-0.24555-0.88337-0.24555-1.422 0-0.65933 0.08403-1.1916 0.25208-1.5967 0.16806-0.40939 0.52357-0.614 1.0665-0.614 0.37921 0 0.67009 0.12059 0.87262 0.36192 0.20253 0.23702 0.30596 0.52578 0.31027 0.86622h1.7581c-0.0086-0.84032-0.2822-1.5061-0.82085-1.9973-0.53434-0.49127-1.2303-0.73691-2.0878-0.73691zm-13.432 0.12918v7.382c0 0.60331-0.28223 0.90492-0.84674 0.90492-0.19822 0-0.39643-0.01939-0.59465-0.05818v1.4803c0.29734 0.07326 0.60972 0.10983 0.93722 0.10983 0.75411 0 1.338-0.20894 1.7517-0.62695 0.41799-0.41801 0.62707-1.0214 0.62707-1.81v-7.382zm-4.57 1.3769c0.53003 0 0.90284 0.20462 1.1183 0.614 0.21977 0.40508 0.32962 0.88561 0.32962 1.4415 0 0.66364-0.10985 1.1937-0.32962 1.5902-0.21546 0.39646-0.58395 0.59465-1.1054 0.59465-0.53004 0-0.90494-0.19819-1.1247-0.59465-0.21546-0.40077-0.32321-0.88562-0.32321-1.4545 0-0.65072 0.10775-1.1786 0.32321-1.5836 0.21546-0.40508 0.58604-0.6076 1.1118-0.6076zm10.917 0c0.43092 0 0.7454 0.12492 0.94362 0.37486 0.20254 0.24563 0.30819 0.5559 0.3168 0.93081v0.14226h-2.5985c0.06464-0.43094 0.20477-0.78 0.42023-1.0472 0.21546-0.26718 0.52142-0.40075 0.91786-0.40075z" fill="#666" stroke-width=".1773" style="shape-inside:url(#rect2964);white-space:pre"/>
<path d="m16.933 38.766h1.5478q1.2383 0 1.7336-0.49531 0.55722-0.55722 0.55722-1.7955v-15.912q0-1.2383-0.49531-1.7336-0.49531-0.55722-1.7336-0.55722h-1.3002v-4.6435h7.9249l0.37148 4.1482q2.1051-2.6623 4.7673-3.8386 2.7242-1.2383 6.5628-1.2383 9.9062 0 9.9062 10.278v13.497q0 1.2383 0.49531 1.7955 0.55722 0.49531 1.7955 0.49531h1.7336v4.5816h-10.649v-20.308q0-3.0957-1.3002-4.4578-1.3002-1.424-4.2101-1.424-3.1576 0-5.2007 1.7955-2.0431 1.7955-2.0431 5.015v12.507q0 1.2383 0.43339 1.7955 0.49531 0.49531 1.6717 0.49531h1.6098v4.5816h-14.178z" fill="#346598" stroke-linecap="round" stroke-width=".16123"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

+7
View File
@@ -61,6 +61,13 @@ more contributions are listed on the project's <a href="https://crowdin.com/proj
<a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)
</p>
<h3>Fonts</h3>
<p>The font used for the main novelWriter logo, mimetype and text banners is:</p>
<p>
Pridi by Cadson Demak (Open Font License, Version 1.1)
</p>
<h3>Special Mentions</h3>
<p>Additional thanks to <a href="https://github.com/johnblommers">@johnblommers</a> who was an early user and who has provided a lot
+7 -37
View File
@@ -2,46 +2,16 @@
<html>
<body>
<h2>Release Notes for 2.1</h2>
<p><i>Released on 17 October 2023</i></p>
<h2>Release Notes for 2.2 Beta 1</h2>
<p><i>Released on 11 November 2023</i></p>
<p>Scroll down for <a href="#patch">Patch Notes</a></p>
<p>The primary focus of this release has been a complete redesign of the Build Tool, that is, the
tool that assembles your project into a manuscript document. The new tool, called the "Manuscript
Build Tool" allows you to define multiple build definitions for your project. The build definitions
are edited in a new Manuscript Build Settings dialog, with a lot more options than the old tool.</p>
<p>The reason for this redesign is a long list of feature requests that could not easily be
accommodated in the old, much simpler tool. Far from all the features have been added yet, but now
that the new tool is in place, they will be gradually added in the coming releases.</p>
<p>The key feature added in this release is the extended control you now have for selecting exactly
what part of your project is included in a given build definition. You have the same filters for
selecting documents and notes, and turning on or off root folders as before, but you can now easily
override on a per-document basis what is included or excluded in addition to the filter.</p>
<p>A second major improvement is a better tool to format your manuscript headings. You no longer
have to look up formatting codes and add them manually. Instead, there is now a heading format
editor in the Build Settings dialog for creating the header format, with syntax highlighting
included.</p>
<h3>Other Changes</h3>
<p>Among other features is a new option to duplicate documents and folders in the project tree. The
duplicate feature is available from the right-click menu. A proper light colour theme has also been
added. In most cases it will be the same as the default theme, depending on your platform.</p>
<p>There are other, minor improvements as well, and a lot of code improvements under the hood. For
a full list of changes, see the detailed changelogs.</p>
<p>This is a beta release of the next release version, and is intended for testing purposes. Please
be careful when using this version on live writing projects, and make sure you take frequent
backups.</p>
<p>Please check the changelog for an overview of changes. The full release notes will be added to
the final release.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<a name="patch"></a><h2>Patch Notes</h2>
<h3>Patch 2.2.1 &ndash; 5 November 2023</h3>
<p>This is a patch release that fixes a layout issue and internationalisation issues with the new
Manuscript Build tool. It also fixes a number of issues related to bugs in the underlying Qt
framework that affects drag and drop functionality in the project tree. These issues were mostly
only affecting Debian Linux package releases.</p>
<p>Other, minor issues related to updating the editor on colour theme change and project word list
changes have been fixed as well. See the full changelog for more details.</p>
</body>
</html>
+3
View File
@@ -148,6 +148,7 @@ class Config:
self.autoScroll = False # Typewriter-like scrolling
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
self.scrollPastEnd = True # Scroll past end of document, and centre cursor
self.wordCountTimer = 5.0 # Interval for word count update in seconds
self.incNotesWCount = True # The status bar word count includes notes
@@ -577,6 +578,7 @@ class Config:
self.doReplaceDots = conf.rdBool(sec, "repdots", self.doReplaceDots)
self.autoScroll = conf.rdBool(sec, "autoscroll", self.autoScroll)
self.autoScrollPos = conf.rdInt(sec, "autoscrollpos", self.autoScrollPos)
self.scrollPastEnd = conf.rdBool(sec, "scrollpastend", self.scrollPastEnd)
self.fmtSQuoteOpen = conf.rdStr(sec, "fmtsquoteopen", self.fmtSQuoteOpen)
self.fmtSQuoteClose = conf.rdStr(sec, "fmtsquoteclose", self.fmtSQuoteClose)
self.fmtDQuoteOpen = conf.rdStr(sec, "fmtdquoteopen", self.fmtDQuoteOpen)
@@ -701,6 +703,7 @@ class Config:
"repdots": str(self.doReplaceDots),
"autoscroll": str(self.autoScroll),
"autoscrollpos": str(self.autoScrollPos),
"scrollpastend": str(self.scrollPastEnd),
"fmtsquoteopen": str(self.fmtSQuoteOpen),
"fmtsquoteclose": str(self.fmtSQuoteClose),
"fmtdquoteopen": str(self.fmtDQuoteOpen),
+1 -2
View File
@@ -327,8 +327,7 @@ class nwQuotes:
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents.
"""
"""Supported unicode character constants and their HTML equivalents."""
# Unicode Constants
# =================
+39 -23
View File
@@ -64,7 +64,6 @@ SETTINGS_TEMPLATE = {
"text.includeKeywords": (bool, False),
"text.includeBodyText": (bool, True),
"text.addNoteHeadings": (bool, True),
"format.buildLang": (str, "en_GB"),
"format.textFont": (str, CONFIG.textFont),
"format.textSize": (int, 12),
"format.lineHeight": (float, 1.15, 0.75, 3.0),
@@ -107,7 +106,6 @@ SETTINGS_LABELS = {
"text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
"format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"),
"format.buildLang": QT_TRANSLATE_NOOP("Builds", "Language"),
"format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"),
"format.textSize": QT_TRANSLATE_NOOP("Builds", "Font Size"),
"format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"),
@@ -158,6 +156,7 @@ class BuildSettings:
self._uuid = str(uuid.uuid4())
self._path = Path.home()
self._build = ""
self._order = 0
self._format = nwBuildFmt.ODT
self._skipRoot = set()
self._excluded = set()
@@ -166,20 +165,32 @@ class BuildSettings:
self._changed = False
return
@classmethod
def fromDict(cls, data: dict) -> BuildSettings:
"""Create a build settings object from a dict."""
cls = BuildSettings()
cls.unpack(data)
return cls
##
# Properties
##
@property
def name(self) -> str:
"""The build name."""
"""Return the build name."""
return self._name
@property
def buildID(self) -> str:
"""The build ID as a UUID."""
"""Return the build ID as a UUID."""
return self._uuid
@property
def order(self) -> int:
"""Return the build order."""
return self._order
@property
def lastPath(self) -> Path:
"""The last used build path."""
@@ -253,6 +264,12 @@ class BuildSettings:
self._uuid = value
return
def setOrder(self, value: int) -> None:
"""Set the build order."""
if isinstance(value, int):
self._order = value
return
def setLastPath(self, path: Path | str | None) -> None:
"""Set the last used build path."""
if isinstance(path, str):
@@ -400,6 +417,7 @@ class BuildSettings:
"uuid": self._uuid,
"path": str(self._path),
"build": self._build,
"order": self._order,
"format": self._format.name,
"settings": self._settings.copy(),
"content": {
@@ -411,14 +429,15 @@ class BuildSettings:
def unpack(self, data: dict) -> None:
"""Unpack a dictionary and populate the class."""
content = data.get("content", {})
settings = data.get("settings", {})
content = data.get("content", {})
included = content.get("included", [])
excluded = content.get("excluded", [])
skipRoot = content.get("skipRoot", [])
self.setName(data.get("name", ""))
self.setBuildID(data.get("uuid", ""))
self.setOrder(data.get("order", 0))
self.setLastPath(data.get("path", None))
self.setLastBuildName(data.get("build", ""))
@@ -455,9 +474,9 @@ class BuildCollection:
def __init__(self, project: NWProject) -> None:
self._project = project
self._builds = {}
self._lastBuild = ""
self._defaultBuild = ""
self._builds: dict[str, BuildSettings] = {}
self._loadCollection()
return
@@ -485,21 +504,19 @@ class BuildCollection:
def getBuild(self, buildID: str) -> BuildSettings | None:
"""Get a specific build settings object."""
if buildID not in self._builds:
return None
build = BuildSettings()
build.unpack(self._builds[buildID])
return build
return self._builds.get(buildID, None)
##
# Setters
##
def setLastBuild(self, buildID: str) -> None:
def setBuildsState(self, lastBuild: str, order: list[str]) -> None:
"""Set the last active build id."""
if buildID != self._lastBuild:
self._lastBuild = buildID
self._saveCollection()
for i, key in enumerate(order):
if build := self._builds.get(key):
build.setOrder(i)
self._lastBuild = lastBuild
self._saveCollection()
return
def setDefaultBuild(self, buildID: str) -> None:
@@ -512,8 +529,7 @@ class BuildCollection:
def setBuild(self, build: BuildSettings) -> None:
"""Set build settings data in the collection."""
if isinstance(build, BuildSettings):
buildID = build.buildID
self._builds[buildID] = build.pack()
self._builds[build.buildID] = build
self._saveCollection()
return
@@ -522,15 +538,15 @@ class BuildCollection:
##
def removeBuild(self, buildID: str) -> None:
"""Remove the a build from the collection."""
"""Remove a build from the collection."""
self._builds.pop(buildID, None)
self._saveCollection()
return
def builds(self) -> Iterable[tuple[str, str]]:
"""Iterate over all available builds."""
for buildID in self._builds:
yield buildID, self._builds[buildID].get("name", "")
for buildID, build in sorted(self._builds.items(), key=lambda x: x[1].order):
yield buildID, build.name
return
##
@@ -569,7 +585,7 @@ class BuildCollection:
elif key == "defaultBuild":
self._defaultBuild = str(entry)
elif isinstance(entry, dict):
self._builds[key] = entry
self._builds[key] = BuildSettings.fromDict(entry)
return True
@@ -581,11 +597,11 @@ class BuildCollection:
logger.debug("Saving builds file")
try:
data = {
data: dict[str, str | dict] = {
"lastBuild": self._lastBuild,
"defaultBuild": self._defaultBuild,
}
data.update(self._builds)
data.update({k: b.pack() for k, b in self._builds.items()})
with open(buildsFile, mode="w+", encoding="utf-8") as outFile:
outFile.write(jsonEncode({"novelWriter.builds": data}, nmax=4))
except Exception:
+2
View File
@@ -353,11 +353,13 @@ class ProjectBuilder:
projName = data.get("projName", lblNewProject)
projTitle = data.get("projTitle", lblNewProject)
projAuthor = data.get("projAuthor", "")
projLang = data.get("projLang", "en_GB")
project.data.setUuid(None)
project.data.setName(projName)
project.data.setTitle(projTitle)
project.data.setAuthor(projAuthor)
project.data.setLanguage(projLang)
project.setDefaultStatusImport()
project.session.startSession()
+1 -5
View File
@@ -248,13 +248,9 @@ class NWBuildDocument:
def _setupBuild(self, bldObj: Tokenizer) -> dict:
"""Configure the build object."""
# Get Settings
buildLang = self._build.getStr("format.buildLang")
textFont = self._build.getStr("format.textFont")
textSize = self._build.getInt("format.textSize")
# The language lookup dict is reloaded if needed
self._project.setProjectLang(buildLang)
fontFamily = textFont or CONFIG.textFont
bldFont = QFont(fontFamily, textSize)
fontInfo = QFontInfo(bldFont)
@@ -287,7 +283,7 @@ class NWBuildDocument:
if isinstance(bldObj, ToOdt):
bldObj.setColourHeaders(self._build.getBool("odt.addColours"))
bldObj.setLanguage(buildLang)
bldObj.setLanguage(self._project.data.language)
scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
+2 -2
View File
@@ -152,14 +152,14 @@ class NWProjectData:
"""Return the initial count of words for novel and note
documents.
"""
return tuple(self._initCounts)
return self._initCounts[0], self._initCounts[1]
@property
def currCounts(self) -> tuple[int, int]:
"""Return the current count of words for novel and note
documents.
"""
return tuple(self._currCounts)
return self._currCounts[0], self._currCounts[1]
@property
def lastHandle(self) -> dict[str, str | None]:
+1 -2
View File
@@ -456,11 +456,10 @@ class ToHtml(Tokenizer):
def _formatSynopsis(self, text: str) -> str:
"""Apply HTML formatting to synopsis."""
sSynop = self._localLookup("Synopsis")
if self._genMode == self.M_PREVIEW:
sSynop = self._trSynopsis
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
else:
sSynop = self._localLookup("Synopsis")
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
def _formatComments(self, text: str) -> str:
-3
View File
@@ -167,9 +167,6 @@ class Tokenizer(ABC):
self._localLookup = self._project.localLookup
self.tr = partial(QCoreApplication.translate, "Tokenizer")
# Cached Translations
self._trSynopsis = self.tr("Synopsis")
# Format RegEx
self._rxMarkdown = [
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
+80 -82
View File
@@ -26,7 +26,7 @@ from __future__ import annotations
import logging
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import (
QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox,
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
@@ -43,12 +43,11 @@ logger = logging.getLogger(__name__)
class GuiPreferences(NPagedDialog):
def __init__(self, mainGui):
super().__init__(parent=mainGui)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
logger.debug("Create: GuiPreferences")
self.setObjectName("GuiPreferences")
self.setWindowTitle(self.tr("Preferences"))
self.tabGeneral = GuiPreferencesGeneral(self)
@@ -93,26 +92,27 @@ class GuiPreferences(NPagedDialog):
##
@property
def updateTheme(self):
def updateTheme(self) -> bool:
return self._updateTheme
@property
def updateSyntax(self):
def updateSyntax(self) -> bool:
return self._updateSyntax
@property
def needsRestart(self):
def needsRestart(self) -> bool:
return self._needsRestart
@property
def refreshTree(self):
def refreshTree(self) -> bool:
return self._refreshTree
##
# Slots
# Private Slots
##
def _doSave(self):
@pyqtSlot()
def _doSave(self) -> None:
"""Trigger all the save functions in the tabs, and collect the
status of the saves.
"""
@@ -132,9 +132,9 @@ class GuiPreferences(NPagedDialog):
return
def _doClose(self):
"""Close the preferences without saving the changes.
"""
@pyqtSlot()
def _doClose(self) -> None:
"""Close the preferences without saving the changes."""
self._saveWindowSize()
self.reject()
return
@@ -143,9 +143,8 @@ class GuiPreferences(NPagedDialog):
# Internal Functions
##
def _saveWindowSize(self):
"""Save the dialog window size.
"""
def _saveWindowSize(self) -> None:
"""Save the dialog window size."""
CONFIG.setPreferencesWinSize(self.width(), self.height())
return
@@ -154,7 +153,7 @@ class GuiPreferences(NPagedDialog):
class GuiPreferencesGeneral(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
self.prefsGui = prefsGui
@@ -285,9 +284,8 @@ class GuiPreferencesGeneral(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
guiLocale = self.guiLocale.currentData()
guiTheme = self.guiTheme.currentData()
guiSyntax = self.guiSyntax.currentData()
@@ -316,12 +314,12 @@ class GuiPreferencesGeneral(QWidget):
return
##
# Slots
# Private Slots
##
def _selectFont(self):
"""Open the QFontDialog and set a font for the font style.
"""
@pyqtSlot()
def _selectFont(self) -> None:
"""Open the QFontDialog and set a font for the font style."""
currFont = QFont()
currFont.setFamily(CONFIG.guiFont)
currFont.setPointSize(CONFIG.guiFontSize)
@@ -336,7 +334,7 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
# The Form
@@ -438,9 +436,8 @@ class GuiPreferencesProjects(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
# Automatic Save
CONFIG.autoSaveDoc = self.autoSaveDoc.value()
CONFIG.autoSaveProj = self.autoSaveProj.value()
@@ -457,12 +454,12 @@ class GuiPreferencesProjects(QWidget):
return
##
# Slots
# Private Slots
##
def _backupFolder(self):
"""Open a dialog to select the backup folder.
"""
@pyqtSlot()
def _backupFolder(self) -> None:
"""Open a dialog to select the backup folder."""
currDir = self.backupPath or ""
newDir = QFileDialog.getExistingDirectory(
self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
@@ -472,15 +469,13 @@ class GuiPreferencesProjects(QWidget):
self.mainForm.setHelpText(
self.backupPathRow, self.tr("Path: {0}").format(self.backupPath)
)
return True
return
return
return False
def _toggledBackupOnClose(self, theState):
"""Enable or disable switch that depends on the backup on close
switch.
"""
self.askBeforeBackup.setEnabled(theState)
@pyqtSlot(bool)
def _toggledBackupOnClose(self, state: bool) -> None:
"""Toggle switch that depends on the backup on close switch."""
self.askBeforeBackup.setEnabled(state)
return
# END Class GuiPreferencesProjects
@@ -488,7 +483,7 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
# The Form
@@ -604,9 +599,8 @@ class GuiPreferencesDocuments(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
# Text Style
CONFIG.setTextFont(self.textFont.text(), self.textSize.value())
@@ -621,12 +615,12 @@ class GuiPreferencesDocuments(QWidget):
return
##
# Slots
# Private Slots
##
@pyqtSlot()
def _selectFont(self):
"""Open the QFontDialog and set a font for the font style.
"""
"""Open the QFontDialog and set a font for the font style."""
currFont = QFont()
currFont.setFamily(CONFIG.textFont)
currFont.setPointSize(CONFIG.textSize)
@@ -642,7 +636,7 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
# The Form
@@ -726,6 +720,15 @@ class GuiPreferencesEditor(QWidget):
# ================
self.mainForm.addGroupLabel(self.tr("Scroll Behaviour"))
# Scroll Past End
self.scrollPastEnd = NSwitch()
self.scrollPastEnd.setChecked(CONFIG.scrollPastEnd)
self.mainForm.addRow(
self.tr("Scroll past end of the document"),
self.scrollPastEnd,
self.tr("Also centres the cursor when scrolling.")
)
# Typewriter Scrolling
self.autoScroll = NSwitch()
self.autoScroll.setChecked(CONFIG.autoScroll)
@@ -750,7 +753,7 @@ class GuiPreferencesEditor(QWidget):
return
def saveValues(self):
def saveValues(self) -> None:
"""Save the values set for this tab."""
# Spell Checking
CONFIG.spellLanguage = self.spellLanguage.currentData()
@@ -766,6 +769,7 @@ class GuiPreferencesEditor(QWidget):
# Scroll Behaviour
CONFIG.autoScroll = self.autoScroll.isChecked()
CONFIG.autoScrollPos = self.autoScrollPos.value()
CONFIG.scrollPastEnd = self.scrollPastEnd.isChecked()
return
@@ -774,7 +778,7 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
self.prefsGui = prefsGui
@@ -840,9 +844,8 @@ class GuiPreferencesSyntax(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
highlightQuotes = self.highlightQuotes.isChecked()
allowOpenSQuote = self.allowOpenSQuote.isChecked()
allowOpenDQuote = self.allowOpenDQuote.isChecked()
@@ -862,15 +865,14 @@ class GuiPreferencesSyntax(QWidget):
return
##
# Slots
# Private Slots
##
def _toggleHighlightQuotes(self, theState):
"""Enables or disables switches controlled by the highlight
quotes switch.
"""
self.allowOpenSQuote.setEnabled(theState)
self.allowOpenDQuote.setEnabled(theState)
@pyqtSlot(bool)
def _toggleHighlightQuotes(self, state: bool) -> None:
"""Toggle switches controlled by the highlight quotes switch."""
self.allowOpenSQuote.setEnabled(state)
self.allowOpenDQuote.setEnabled(state)
return
# END Class GuiPreferencesSyntax
@@ -878,7 +880,7 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
# The Form
@@ -989,9 +991,8 @@ class GuiPreferencesAutomation(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
# Automatic Features
CONFIG.autoSelect = self.autoSelect.isChecked()
CONFIG.doReplace = self.doReplace.isChecked()
@@ -1010,18 +1011,17 @@ class GuiPreferencesAutomation(QWidget):
return
##
# Slots
# Private Slots
##
def _toggleAutoReplaceMain(self, theState):
"""Enables or disables switches controlled by the main auto
replace switch.
"""
self.doReplaceSQuote.setEnabled(theState)
self.doReplaceDQuote.setEnabled(theState)
self.doReplaceDash.setEnabled(theState)
self.doReplaceDots.setEnabled(theState)
self.fmtPadThin.setEnabled(theState)
@pyqtSlot(bool)
def _toggleAutoReplaceMain(self, state: bool) -> None:
"""Toggle switches controlled by the auto replace switch."""
self.doReplaceSQuote.setEnabled(state)
self.doReplaceDQuote.setEnabled(state)
self.doReplaceDash.setEnabled(state)
self.doReplaceDots.setEnabled(state)
self.fmtPadThin.setEnabled(state)
return
# END Class GuiPreferencesAutomation
@@ -1029,7 +1029,7 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget):
def __init__(self, prefsGui):
def __init__(self, prefsGui: GuiPreferences) -> None:
super().__init__(parent=prefsGui)
# The Form
@@ -1113,9 +1113,8 @@ class GuiPreferencesQuotes(QWidget):
return
def saveValues(self):
"""Save the values set for this tab.
"""
def saveValues(self) -> None:
"""Save the values set for this tab."""
# Quotation Style
CONFIG.fmtSQuoteOpen = self.quoteSym["SO"].text()
CONFIG.fmtSQuoteClose = self.quoteSym["SC"].text()
@@ -1124,12 +1123,11 @@ class GuiPreferencesQuotes(QWidget):
return
##
# Slots
# Internal Functions
##
def _getQuote(self, qType):
"""Dialog for single quote open.
"""
def _getQuote(self, qType: str) -> None:
"""Dialog for single quote open."""
qtBox = GuiQuoteSelect(self, currentQuote=self.quoteSym[qType].text())
if qtBox.exec_() == QDialog.Accepted:
self.quoteSym[qType].setText(qtBox.selectedQuote)
+101 -110
View File
@@ -96,21 +96,22 @@ class GuiProjectSettings(NPagedDialog):
return
def __del__(self): # pragma: no cover
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectSettings")
return
##
# Slots
# Private Slots
##
def _doSave(self):
"""Save settings and close dialog.
"""
project = SHARED.project
@pyqtSlot()
def _doSave(self) -> None:
"""Save settings and close dialog."""
project = SHARED.project
projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text()
bookAuthor = self.tabMain.editAuthor.text()
projLang = self.tabMain.projLang.currentData()
spellLang = self.tabMain.spellLang.currentData()
doBackup = not self.tabMain.doBackup.isChecked()
@@ -119,6 +120,7 @@ class GuiProjectSettings(NPagedDialog):
project.data.setAuthor(bookAuthor)
project.data.setDoBackup(doBackup)
project.data.setSpellLang(spellLang)
project.setProjectLang(projLang)
if self.tabStatus.colChanged:
newList, delList = self.tabStatus.getNewList()
@@ -140,9 +142,9 @@ class GuiProjectSettings(NPagedDialog):
return
def _doClose(self):
"""Save settings and close the dialog.
"""
@pyqtSlot()
def _doClose(self) -> None:
"""Save settings and close the dialog."""
self._saveGuiSettings()
self.reject()
return
@@ -151,9 +153,8 @@ class GuiProjectSettings(NPagedDialog):
# Internal Functions
##
def _focusTab(self, tab):
"""Change which is the focused tab.
"""
def _focusTab(self, tab: int) -> None:
"""Change which is the focused tab."""
if tab == self.TAB_MAIN:
self.setCurrentWidget(self.tabMain)
elif tab == self.TAB_STATUS:
@@ -164,9 +165,8 @@ class GuiProjectSettings(NPagedDialog):
self.setCurrentWidget(self.tabReplace)
return
def _saveGuiSettings(self):
"""Save GUI settings.
"""
def _saveGuiSettings(self) -> None:
"""Save GUI settings."""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0))
@@ -187,8 +187,8 @@ class GuiProjectSettings(NPagedDialog):
class GuiProjectEditMain(QWidget):
def __init__(self, projGui):
super().__init__(parent=projGui)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
# The Form
self.mainForm = NConfigLayout()
@@ -230,24 +230,41 @@ class GuiProjectEditMain(QWidget):
self.tr("Change whenever you want!")
)
self.projLang = QComboBox(self)
self.projLang.setMaximumWidth(xW)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
self.mainForm.addRow(
self.tr("Project language"),
self.projLang,
self.tr("Used when building the manuscript.")
)
langIdx = 0
if pData.language is not None:
langIdx = self.projLang.findData(pData.language)
if langIdx == -1:
langIdx = self.projLang.findData("en_GB")
if langIdx != -1:
self.projLang.setCurrentIndex(langIdx)
self.spellLang = QComboBox(self)
self.spellLang.setMaximumWidth(xW)
self.spellLang.addItem(self.tr("Default"), "None")
if CONFIG.hasEnchant:
for tag, language in SHARED.spelling.listDictionaries():
self.spellLang.addItem(language, tag)
self.mainForm.addRow(
self.tr("Spell check language"),
self.spellLang,
self.tr("Overrides main preferences.")
)
spellIdx = 0
langIdx = 0
if pData.spellLang is not None:
spellIdx = self.spellLang.findData(pData.spellLang)
if spellIdx != -1:
self.spellLang.setCurrentIndex(spellIdx)
langIdx = self.spellLang.findData(pData.spellLang)
if langIdx != -1:
self.spellLang.setCurrentIndex(langIdx)
self.doBackup = NSwitch(self)
self.doBackup.setChecked(not pData.doBackup)
@@ -271,8 +288,8 @@ class GuiProjectEditStatus(QWidget):
COL_ROLE = Qt.ItemDataRole.UserRole + 1
NUM_ROLE = Qt.ItemDataRole.UserRole + 2
def __init__(self, projGui, isStatus):
super().__init__(parent=projGui)
def __init__(self, parent: QWidget, isStatus: bool) -> None:
super().__init__(parent=parent)
if isStatus:
self.theStatus = SHARED.project.data.itemStatus
@@ -372,9 +389,8 @@ class GuiProjectEditStatus(QWidget):
return
def getNewList(self):
"""Return list of entries.
"""
def getNewList(self) -> tuple[list, list]:
"""Return list of entries."""
if self.colChanged:
newList = []
for n in range(self.listBox.topLevelItemCount()):
@@ -394,9 +410,8 @@ class GuiProjectEditStatus(QWidget):
##
@pyqtSlot()
def _selectColour(self):
"""Open a dialog to select the status icon colour.
"""
def _selectColour(self) -> None:
"""Open a dialog to select the status icon colour."""
if self.selColour is not None:
newCol = QColorDialog.getColor(
self.selColour, self, self.tr("Select Colour")
@@ -410,17 +425,15 @@ class GuiProjectEditStatus(QWidget):
return
@pyqtSlot()
def _newItem(self):
"""Create a new status item.
"""
def _newItem(self) -> None:
"""Create a new status item."""
self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
self.colChanged = True
return
@pyqtSlot()
def _delItem(self):
"""Delete a status item.
"""
def _delItem(self) -> None:
"""Delete a status item."""
selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem):
iRow = self.listBox.indexOfTopLevelItem(selItem)
@@ -433,9 +446,8 @@ class GuiProjectEditStatus(QWidget):
return
@pyqtSlot()
def _saveItem(self):
"""Save changes made to a status item.
"""
def _saveItem(self) -> None:
"""Save changes made to a status item."""
selItem = self._getSelectedItem()
if isinstance(selItem, QTreeWidgetItem):
selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
@@ -444,11 +456,10 @@ class GuiProjectEditStatus(QWidget):
self.selColour.red(), self.selColour.green(), self.selColour.blue()
))
self.colChanged = True
return
@pyqtSlot()
def _selectedItem(self):
def _selectedItem(self) -> None:
"""Extract the info of a selected item and populate the settings
boxes and button. If no item is selected, clear the form.
"""
@@ -456,7 +467,6 @@ class GuiProjectEditStatus(QWidget):
if isinstance(selItem, QTreeWidgetItem):
cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
name = selItem.text(self.COL_LABEL)
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
self.selColour = QColor(*cols)
@@ -464,31 +474,27 @@ class GuiProjectEditStatus(QWidget):
self.colButton.setIcon(QIcon(pixmap))
self.editName.selectAll()
self.editName.setFocus()
self.editName.setEnabled(True)
self.colButton.setEnabled(True)
self.saveButton.setEnabled(True)
else:
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(100, 100, 100))
self.selColour = QColor(100, 100, 100)
self.editName.setText("")
self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(False)
self.colButton.setEnabled(False)
self.saveButton.setEnabled(False)
return
##
# Internal Functions
##
def _addItem(self, key, name, cols, count):
"""Add a status item to the list.
"""
def _addItem(self, key: str | None, name: str,
cols: tuple[int, int, int], count: int) -> None:
"""Add a status item to the list."""
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
@@ -504,9 +510,8 @@ class GuiProjectEditStatus(QWidget):
return
def _moveItem(self, step):
"""Move and item up or down step.
"""
def _moveItem(self, step: int) -> None:
"""Move and item up or down step."""
selItem = self._getSelectedItem()
if selItem is None:
return
@@ -527,17 +532,15 @@ class GuiProjectEditStatus(QWidget):
return
def _getSelectedItem(self):
"""Get the currently selected item.
"""
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item."""
selItem = self.listBox.selectedItems()
if len(selItem) > 0:
return selItem[0]
return None
def _usageString(self, nUse):
"""Generate usage string.
"""
def _usageString(self, nUse: int) -> str:
"""Generate usage string."""
if nUse == 0:
return self.tr("Not in use")
elif nUse == 1:
@@ -553,8 +556,8 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
def __init__(self, projGui):
super().__init__(parent=projGui)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self.arChanged = False
@@ -635,25 +638,23 @@ class GuiProjectEditReplace(QWidget):
return
def getNewList(self):
"""Extract the list from the widget.
"""
newList = {}
def getNewList(self) -> dict:
"""Extract the list from the widget."""
new = {}
for n in range(self.listBox.topLevelItemCount()):
tItem = self.listBox.topLevelItem(n)
if tItem is not None:
aKey = self._stripNotAllowed(tItem.text(0))
aVal = tItem.text(1)
if len(aKey) > 0:
newList[aKey] = aVal
return newList
new[aKey] = aVal
return new
##
# Internal Functions
##
def _selectedItem(self):
def _selectedItem(self) -> bool:
"""Extract the details from the selected item and populate the
edit form.
"""
@@ -670,63 +671,53 @@ class GuiProjectEditReplace(QWidget):
self.editKey.setFocus()
return True
def _saveEntry(self):
"""Save the form data into the list widget.
"""
def _saveEntry(self) -> None:
"""Save the form data into the list widget."""
selItem = self._getSelectedItem()
if selItem is None:
return False
newKey = self.editKey.text()
newVal = self.editValue.text()
saveKey = self._stripNotAllowed(newKey)
if len(saveKey) > 0 and len(newVal) > 0:
selItem.setText(self.COL_KEY, "<%s>" % saveKey)
selItem.setText(self.COL_REPL, newVal)
self.editKey.clear()
self.editValue.clear()
self.editKey.setEnabled(False)
self.editValue.setEnabled(False)
self.listBox.clearSelection()
self.arChanged = True
if selItem:
newKey = self.editKey.text()
newVal = self.editValue.text()
saveKey = self._stripNotAllowed(newKey)
if len(saveKey) > 0 and len(newVal) > 0:
selItem.setText(self.COL_KEY, "<%s>" % saveKey)
selItem.setText(self.COL_REPL, newVal)
self.editKey.clear()
self.editValue.clear()
self.editKey.setEnabled(False)
self.editValue.setEnabled(False)
self.listBox.clearSelection()
self.arChanged = True
return
def _addEntry(self):
"""Add a new list entry.
"""
def _addEntry(self) -> None:
"""Add a new list entry."""
saveKey = "<keyword%d>" % (self.listBox.topLevelItemCount() + 1)
newVal = ""
newItem = QTreeWidgetItem([saveKey, newVal])
self.listBox.addTopLevelItem(newItem)
return True
return
def _delEntry(self):
"""Delete the selected entry.
"""
def _delEntry(self) -> None:
"""Delete the selected entry."""
selItem = self._getSelectedItem()
if selItem is None:
return False
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem))
self.arChanged = True
return True
if selItem:
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem))
self.arChanged = True
return
def _getSelectedItem(self):
"""Extract the currently selected item.
"""
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Extract the currently selected item."""
selItem = self.listBox.selectedItems()
if len(selItem) == 0:
return None
return selItem[0]
def _stripNotAllowed(self, theKey):
"""Clean up the replace key string.
"""
retKey = ""
for c in theKey:
def _stripNotAllowed(self, key: str) -> str:
"""Clean up the replace key string."""
result = ""
for c in key:
if c.isalnum():
retKey += c
return retKey
result += c
return result
# END Class GuiProjectEditReplace
+99 -56
View File
@@ -10,6 +10,8 @@ Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
Created: 2020-06-27 [0.10] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
Created: 2023-11-06 [2.2b1] MetaCompleter
Created: 2023-11-07 [2.2b1] GuiDocToolBar
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
@@ -67,6 +69,16 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
class _SelectAction(Enum):
NO_DECISION = 0
KEEP_SELECTION = 1
KEEP_POSITION = 2
MOVE_AFTER = 3
# END Class _SelectAction
class GuiDocEditor(QPlainTextEdit):
"""Gui Widget: Main Document Editor"""
@@ -152,7 +164,6 @@ class GuiDocEditor(QPlainTextEdit):
self.setMinimumWidth(CONFIG.pxInt(300))
self.setAutoFillBackground(True)
self.setFrameStyle(QFrame.NoFrame)
self.setCenterOnScroll(True)
# Custom Shortcuts
self.keyContext = QShortcut(self)
@@ -330,7 +341,8 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument.setDefaultTextOption(options)
# Scroll bars
# Scrolling
self.setCenterOnScroll(CONFIG.scrollPastEnd)
if CONFIG.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
@@ -996,7 +1008,9 @@ class GuiDocEditor(QPlainTextEdit):
return
text = block.text()
if text.startswith("@"):
if text.startswith("@") and added + removed == 1:
# Only run on single keypresses, otherwise it will trigger
# at unwanted times when other changes are made to the document
cursor = self.textCursor()
bPos = cursor.positionInBlock()
if bPos > 0:
@@ -1004,8 +1018,10 @@ class GuiDocEditor(QPlainTextEdit):
point = self.cursorRect().bottomRight()
self._completer.move(self.viewport().mapToGlobal(point))
self._completer.setVisible(show)
else:
self._completer.setVisible(False)
elif self._doReplace and added == 1:
if self._doReplace and added == 1:
self._docAutoReplace(text)
return
@@ -1410,17 +1426,28 @@ class GuiDocEditor(QPlainTextEdit):
If more than one block is selected, the formatting is applied to
the first block.
"""
cursor = self._autoSelect()
if not cursor.hasSelection():
logger.warning("No selection made, nothing to do")
return False
cursor = self.textCursor()
posO = cursor.position()
if cursor.hasSelection():
select = _SelectAction.KEEP_SELECTION
else:
cursor = self._autoSelect()
if cursor.hasSelection() and posO == cursor.selectionEnd():
select = _SelectAction.MOVE_AFTER
else:
select = _SelectAction.KEEP_POSITION
posS = cursor.selectionStart()
posE = cursor.selectionEnd()
if self._qDocument.characterAt(posO - 1) == fChar:
logger.warning("Format repetition, cancelling action")
cursor.clearSelection()
cursor.setPosition(posO)
self.setTextCursor(cursor)
return False
blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
cursor.clearSelection()
@@ -1443,34 +1470,26 @@ class GuiDocEditor(QPlainTextEdit):
break
if fLen == min(numA, numB):
self._clearSurrounding(cursor, fLen)
cursor.clearSelection()
cursor.beginEditBlock()
cursor.setPosition(posS)
for i in range(fLen):
cursor.deletePreviousChar()
cursor.setPosition(posE)
for i in range(fLen):
cursor.deletePreviousChar()
cursor.endEditBlock()
cursor.clearSelection()
cursor.setPosition(posO - fLen)
self.setTextCursor(cursor)
else:
self._wrapSelection(fChar*fLen)
self._wrapSelection(fChar*fLen, pos=posO, select=select)
return True
def _clearSurrounding(self, cursor: QTextCursor, nChars: int) -> bool:
"""Clear n characters before and after the cursor."""
if not cursor.hasSelection():
logger.warning("No selection made, nothing to do")
return False
posS = cursor.selectionStart()
posE = cursor.selectionEnd()
cursor.clearSelection()
cursor.beginEditBlock()
cursor.setPosition(posS)
for i in range(nChars):
cursor.deletePreviousChar()
cursor.setPosition(posE)
for i in range(nChars):
cursor.deletePreviousChar()
cursor.endEditBlock()
cursor.clearSelection()
return True
def _wrapSelection(self, before: str, after: str | None = None) -> bool:
def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None,
select: _SelectAction = _SelectAction.NO_DECISION) -> bool:
"""Wrap the selected text in whatever is in tBefore and tAfter.
If there is no selection, the autoSelect setting decides the
action. AutoSelect will select the word under the cursor before
@@ -1479,10 +1498,17 @@ class GuiDocEditor(QPlainTextEdit):
if after is None:
after = before
cursor = self._autoSelect()
if not cursor.hasSelection():
logger.warning("No selection made, nothing to do")
return False
cursor = self.textCursor()
posO = pos if isinstance(pos, int) else cursor.position()
if select == _SelectAction.NO_DECISION:
if cursor.hasSelection():
select = _SelectAction.KEEP_SELECTION
else:
cursor = self._autoSelect()
if cursor.hasSelection() and posO == cursor.selectionEnd():
select = _SelectAction.MOVE_AFTER
else:
select = _SelectAction.KEEP_POSITION
posS = cursor.selectionStart()
posE = cursor.selectionEnd()
@@ -1500,8 +1526,14 @@ class GuiDocEditor(QPlainTextEdit):
cursor.insertText(before)
cursor.endEditBlock()
cursor.setPosition(posE + len(before), QTextCursor.MoveAnchor)
cursor.setPosition(posS + len(before), QTextCursor.KeepAnchor)
if select == _SelectAction.MOVE_AFTER:
cursor.setPosition(posE + len(before + after))
elif select == _SelectAction.KEEP_SELECTION:
cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor)
cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor)
elif select == _SelectAction.KEEP_POSITION:
cursor.setPosition(posO + len(before))
self.setTextCursor(cursor)
return True
@@ -1908,27 +1940,38 @@ class GuiDocEditor(QPlainTextEdit):
def _autoSelect(self) -> QTextCursor:
"""Return a cursor which may or may not have a selection based
on user settings and document action.
on user settings and document action. The selection will be the
word closest to the cursor consisting of alphanumerical unicode
characters.
"""
cursor = self.textCursor()
if CONFIG.autoSelect and not cursor.hasSelection():
cursor.select(QTextCursor.WordUnderCursor)
posS = cursor.selectionStart()
posE = cursor.selectionEnd()
cPos = cursor.position()
bPos = cursor.block().position()
bLen = cursor.block().length()
# Underscore counts as a part of the word, so check that the
# selection isn't wrapped in italics markers.
reSelect = False
if self._qDocument.characterAt(posS) == "_":
posS += 1
reSelect = True
if self._qDocument.characterAt(posE) == "_":
posE -= 1
reSelect = True
if reSelect:
cursor.clearSelection()
cursor.setPosition(posS, QTextCursor.MoveAnchor)
cursor.setPosition(posE-1, QTextCursor.KeepAnchor)
# Scan backwards
sPos = cPos
for i in range(cPos - bPos):
sPos = cPos - i - 1
if not self._qDocument.characterAt(sPos).isalnum():
sPos += 1
break
# Scan forwards
ePos = cPos
for i in range(bPos + bLen - cPos):
ePos = cPos + i
if not self._qDocument.characterAt(ePos).isalnum():
break
if ePos - sPos <= 0:
# No selection possible
return cursor
cursor.clearSelection()
cursor.setPosition(sPos, QTextCursor.MoveAnchor)
cursor.setPosition(ePos, QTextCursor.KeepAnchor)
self.setTextCursor(cursor)
+36 -20
View File
@@ -54,6 +54,7 @@ class GuiMainMenu(QMenuBar):
requestDocInsert = pyqtSignal(nwDocInsert)
requestDocInsertText = pyqtSignal(str)
requestDocKeyWordInsert = pyqtSignal(str)
requestFocusChange = pyqtSignal(nwWidget)
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
@@ -173,7 +174,7 @@ class GuiMainMenu(QMenuBar):
# Project > Delete
self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item"))
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.setShortcuts(["Ctrl+Del", "Ctrl+Shift+Del"]) # Latter is deprecated
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
# Project > Empty Trash
@@ -246,12 +247,16 @@ class GuiMainMenu(QMenuBar):
# Edit > Undo
self.aEditUndo = self.editMenu.addAction(self.tr("Undo"))
self.aEditUndo.setShortcut("Ctrl+Z")
self.aEditUndo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.UNDO))
self.aEditUndo.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.UNDO)
)
# Edit > Redo
self.aEditRedo = self.editMenu.addAction(self.tr("Redo"))
self.aEditRedo.setShortcut("Ctrl+Y")
self.aEditRedo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.REDO))
self.aEditRedo.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.REDO)
)
# Edit > Separator
self.editMenu.addSeparator()
@@ -259,17 +264,23 @@ class GuiMainMenu(QMenuBar):
# Edit > Cut
self.aEditCut = self.editMenu.addAction(self.tr("Cut"))
self.aEditCut.setShortcut("Ctrl+X")
self.aEditCut.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.CUT))
self.aEditCut.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.CUT)
)
# Edit > Copy
self.aEditCopy = self.editMenu.addAction(self.tr("Copy"))
self.aEditCopy.setShortcut("Ctrl+C")
self.aEditCopy.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.COPY))
self.aEditCopy.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.COPY)
)
# Edit > Paste
self.aEditPaste = self.editMenu.addAction(self.tr("Paste"))
self.aEditPaste.setShortcut("Ctrl+V")
self.aEditPaste.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.PASTE))
self.aEditPaste.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.PASTE)
)
# Edit > Separator
self.editMenu.addSeparator()
@@ -277,12 +288,16 @@ class GuiMainMenu(QMenuBar):
# Edit > Select All
self.aSelectAll = self.editMenu.addAction(self.tr("Select All"))
self.aSelectAll.setShortcut("Ctrl+A")
self.aSelectAll.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_ALL))
self.aSelectAll.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.SEL_ALL)
)
# Edit > Select Paragraph
self.aSelectPar = self.editMenu.addAction(self.tr("Select Paragraph"))
self.aSelectPar.setShortcut("Ctrl+Shift+A")
self.aSelectPar.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_PARA))
self.aSelectPar.triggered.connect(
lambda: self.requestDocAction.emit(nwDocAction.SEL_PARA)
)
return
@@ -293,23 +308,24 @@ class GuiMainMenu(QMenuBar):
# View > TreeView
self.aFocusTree = self.viewMenu.addAction(self.tr("Go to Project Tree"))
self.aFocusTree.setShortcut("Ctrl+Alt+1" if CONFIG.osWindows else "Alt+1")
self.aFocusTree.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.TREE))
self.aFocusTree.setShortcut("Ctrl+T")
self.aFocusTree.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.TREE)
)
# View > Document Pane 1
# View > Document Editor
self.aFocusEditor = self.viewMenu.addAction(self.tr("Go to Document Editor"))
self.aFocusEditor.setShortcut("Ctrl+Alt+2" if CONFIG.osWindows else "Alt+2")
self.aFocusEditor.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.EDITOR))
# View > Document Pane 2
self.aFocusView = self.viewMenu.addAction(self.tr("Go to Document Viewer"))
self.aFocusView.setShortcut("Ctrl+Alt+3" if CONFIG.osWindows else "Alt+3")
self.aFocusView.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.VIEWER))
self.aFocusEditor.setShortcut("Ctrl+E")
self.aFocusEditor.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.EDITOR)
)
# View > Outline
self.aFocusOutline = self.viewMenu.addAction(self.tr("Go to Outline"))
self.aFocusOutline.setShortcut("Ctrl+Alt+4" if CONFIG.osWindows else "Alt+4")
self.aFocusOutline.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.OUTLINE))
self.aFocusOutline.setShortcut("Ctrl+Shift+T")
self.aFocusOutline.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.OUTLINE)
)
# View > Separator
self.viewMenu.addSeparator()
+1 -1
View File
@@ -189,7 +189,7 @@ class GuiProjectView(QWidget):
self.projTree.buildTree()
return
def setFocus(self) -> None:
def setTreeFocus(self) -> None:
"""Forward the set focus call to the tree widget."""
self.projTree.setFocus()
return
+8 -8
View File
@@ -57,32 +57,32 @@ class GuiSideBar(QWidget):
# Buttons
self.tbProject = QToolButton(self)
self.tbProject.setToolTip(self.tr("Project Tree View"))
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
self.tbProject.setIconSize(iconSize)
self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
self.tbNovel = QToolButton(self)
self.tbNovel.setToolTip(self.tr("Novel Tree View"))
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
self.tbNovel.setIconSize(iconSize)
self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
self.tbOutline = QToolButton(self)
self.tbOutline.setToolTip(self.tr("Novel Outline View"))
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
self.tbOutline.setIconSize(iconSize)
self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
self.tbBuild = QToolButton(self)
self.tbBuild.setToolTip(self.tr("Build Manuscript"))
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
self.tbBuild.setIconSize(iconSize)
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
self.tbDetails = QToolButton(self)
self.tbDetails.setToolTip(self.tr("Project Details"))
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details")))
self.tbDetails.setIconSize(iconSize)
self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog)
self.tbStats = QToolButton(self)
self.tbStats.setToolTip(self.tr("Writing Statistics"))
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
self.tbStats.setIconSize(iconSize)
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
@@ -111,7 +111,7 @@ class GuiSideBar(QWidget):
self.outerBox.addWidget(self.tbDetails)
self.outerBox.addWidget(self.tbStats)
self.outerBox.addWidget(self.tbSettings)
self.outerBox.setContentsMargins(0, 0, CONFIG.pxInt(2), 0)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(CONFIG.pxInt(4))
self.setLayout(self.outerBox)
@@ -131,7 +131,7 @@ class GuiSideBar(QWidget):
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(CONFIG.pxInt(4), fadeCol.red(), fadeCol.green(), fadeCol.blue())
).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
+44 -33
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence
from PyQt5.QtGui import QCloseEvent, QCursor, QIcon
from PyQt5.QtWidgets import (
QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut,
QSplitter, QStackedWidget, QVBoxLayout, QWidget, qApp
@@ -247,6 +247,7 @@ class GuiMain(QMainWindow):
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert)
self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord)
self.mainMenu.requestFocusChange.connect(self.switchFocus)
self.sideBar.viewChangeRequested.connect(self._changeView)
@@ -300,17 +301,17 @@ class GuiMain(QMainWindow):
# Shortcuts and Actions
self._connectMenuActions()
keyReturn = QShortcut(self)
keyReturn.setKey(QKeySequence(Qt.Key_Return))
keyReturn.activated.connect(self._keyPressReturn)
self.keyReturn = QShortcut(self)
self.keyReturn.setKey(Qt.Key.Key_Return)
self.keyReturn.activated.connect(self._keyPressReturn)
keyEnter = QShortcut(self)
keyEnter.setKey(QKeySequence(Qt.Key_Enter))
keyEnter.activated.connect(self._keyPressReturn)
self.keyEnter = QShortcut(self)
self.keyEnter.setKey(Qt.Key.Key_Enter)
self.keyEnter.activated.connect(self._keyPressReturn)
keyEscape = QShortcut(self)
keyEscape.setKey(QKeySequence(Qt.Key_Escape))
keyEscape.activated.connect(self._keyPressEscape)
self.keyEscape = QShortcut(self)
self.keyEscape.setKey(Qt.Key.Key_Escape)
self.keyEscape.activated.connect(self._keyPressEscape)
# Check that config loaded fine
self.reportConfErr()
@@ -1093,25 +1094,6 @@ class GuiMain(QMainWindow):
return True
def switchFocus(self, paneNo: nwWidget) -> None:
"""Switch focus between main GUI views."""
if paneNo == nwWidget.TREE:
tabIdx = self.projStack.currentIndex()
if tabIdx == self.idxProjView:
self.projView.setFocus()
elif tabIdx == self.idxNovelView:
self.novelView.setTreeFocus()
elif paneNo == nwWidget.EDITOR:
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
elif paneNo == nwWidget.VIEWER:
self._changeView(nwView.EDITOR)
self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE:
self._changeView(nwView.OUTLINE)
self.outlineView.setTreeFocus()
return
def closeDocViewer(self, byUser: bool = True) -> bool:
"""Close the document view panel."""
self.docViewer.clearViewer()
@@ -1189,6 +1171,33 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(nwWidget)
def switchFocus(self, paneNo: nwWidget) -> None:
"""Switch focus between main GUI views."""
if paneNo == nwWidget.TREE:
if self.projStack.currentWidget() is self.projView:
if self.projView.treeHasFocus():
self._changeView(nwView.NOVEL)
self.novelView.setTreeFocus()
else:
self.projView.setTreeFocus()
else:
if self.novelView.treeHasFocus():
self._changeView(nwView.PROJECT)
self.projView.setTreeFocus()
else:
self.novelView.setTreeFocus()
elif paneNo == nwWidget.EDITOR:
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
elif paneNo == nwWidget.VIEWER:
self._changeView(nwView.EDITOR)
self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE:
self._changeView(nwView.OUTLINE)
self.outlineView.setTreeFocus()
return
##
# Private Slots
##
@@ -1224,18 +1233,14 @@ class GuiMain(QMainWindow):
if view == nwView.EDITOR:
# Only change the main stack, but not the project stack
self.mainStack.setCurrentWidget(self.splitMain)
elif view == nwView.PROJECT:
self.mainStack.setCurrentWidget(self.splitMain)
self.projStack.setCurrentWidget(self.projView)
elif view == nwView.NOVEL:
self.mainStack.setCurrentWidget(self.splitMain)
self.projStack.setCurrentWidget(self.novelView)
elif view == nwView.OUTLINE:
self.mainStack.setCurrentWidget(self.outlineView)
return
@pyqtSlot(nwDocAction)
@@ -1482,6 +1487,12 @@ class GuiMain(QMainWindow):
projData["numChapters"] = newProj.field("numChapters")
projData["numScenes"] = newProj.field("numScenes")
try:
langIdx = newProj.field("projLang")
projData["projLang"] = CONFIG.listLanguages(CONFIG.LANG_PROJ)[langIdx][0]
except Exception:
projData["projLang"] = "en_GB"
return projData
def _getTagSource(self, tag: str) -> tuple[str | None, str | None]:
+10 -2
View File
@@ -144,6 +144,8 @@ class GuiManuscript(QDialog):
self.buildList.setIconSize(QSize(iPx, iPx))
self.buildList.doubleClicked.connect(self._editSelectedBuild)
self.buildList.currentItemChanged.connect(self._updateBuildDetails)
self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
self.buildDetails = _DetailsWidget(self)
self.buildDetails.setColumnWidth(
@@ -417,9 +419,15 @@ class GuiManuscript(QDialog):
"""Save the user GUI settings."""
logger.debug("Saving GuiManuscript settings")
buildOrder = []
for i in range(self.buildList.count()):
if item := self.buildList.item(i):
buildOrder.append(item.data(self.D_KEY))
current = self.buildList.currentItem()
if isinstance(current, QListWidgetItem):
self._builds.setLastBuild(current.data(self.D_KEY))
lastBuild = current.data(self.D_KEY) if isinstance(current, QListWidgetItem) else ""
self._builds.setBuildsState(lastBuild, buildOrder)
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
-14
View File
@@ -974,15 +974,6 @@ class _FormatTab(QWidget):
self.formFormat = NConfigLayout()
self.formFormat.addGroupLabel(self._build.getLabel("format.grpFormat"))
# Build Language
self.buildLang = QComboBox()
langauges = CONFIG.listLanguages(CONFIG.LANG_PROJ)
self.buildLang.addItem("[%s]" % self.tr("Not Set"), "None")
for langID, langName in langauges:
self.buildLang.addItem(langName, langID)
self.formFormat.addRow(self._build.getLabel("format.buildLang"), self.buildLang)
# Font Family
self.textFont = QLineEdit()
self.textFont.setReadOnly(True)
@@ -1103,10 +1094,6 @@ class _FormatTab(QWidget):
def loadContent(self) -> None:
"""Populate the widgets."""
langIdx = self.buildLang.findData(self._build.getStr("format.buildLang"))
if langIdx != -1:
self.buildLang.setCurrentIndex(langIdx)
textFont = self._build.getStr("format.textFont")
if not textFont:
textFont = str(CONFIG.textFont)
@@ -1146,7 +1133,6 @@ class _FormatTab(QWidget):
def saveContent(self) -> None:
"""Save choices back into build object."""
self._build.setValue("format.buildLang", str(self.buildLang.currentData()))
self._build.setValue("format.textFont", self.textFont.text())
self._build.setValue("format.textSize", self.textSize.value())
self._build.setValue("format.lineHeight", self.lineHeight.value())
+16 -2
View File
@@ -28,8 +28,9 @@ import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage
QComboBox, QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard,
QWizardPage
)
from novelwriter import CONFIG, SHARED
@@ -127,15 +128,28 @@ class ProjWizardIntroPage(QWizardPage):
self.projAuthor.setFixedWidth(xW)
self.projAuthor.setPlaceholderText(self.tr("Optional"))
self.projLang = QComboBox(self)
self.projLang.setMaximumWidth(xW)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
langIdx = self.projLang.findData(CONFIG.guiLocale)
if langIdx == -1:
langIdx = self.projLang.findData("en_GB")
if langIdx != -1:
self.projLang.setCurrentIndex(langIdx)
self.mainForm = QFormLayout()
self.mainForm.addRow(self.tr("Project Name"), self.projName)
self.mainForm.addRow(self.tr("Novel Title"), self.projTitle)
self.mainForm.addRow(self.tr("Author(s)"), self.projAuthor)
self.mainForm.addRow(self.tr("Language"), self.projLang)
self.mainForm.setVerticalSpacing(fS)
self.registerField("projName*", self.projName)
self.registerField("projTitle", self.projTitle)
self.registerField("projAuthor", self.projAuthor)
self.registerField("projLang", self.projLang)
# Assemble
self.outerBox = QVBoxLayout()