Merge branch 'main' into release_1.0rc2

This commit is contained in:
Veronica K. B. Olsen
2020-12-12 11:32:22 +01:00
70 changed files with 5514 additions and 1851 deletions
+20 -18
View File
@@ -58,20 +58,21 @@ from nw.config import Config
#
__package__ = "nw"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.0rc2"
__hexversion__ = "0x010000c2"
__date__ = "2020-11-29"
__date__ = "2020-12-13"
__status__ = "Beta"
__isstable__ = False
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
__sourceurl__ = "https://github.com/vkbo/novelWriter"
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
__domain__ = "novelwriter.io"
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
__docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [
"Veronica Berglyd Olsen (developer)",
@@ -246,19 +247,20 @@ def main(sysArgs=None):
errorCode |= 32
if errorData:
if not testMode:
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
errApp.exec_()
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
for errMsg in errorData:
logger.critical(errMsg)
errApp.exec_()
sys.exit(errorCode)
# Finish initialising config
@@ -293,4 +295,4 @@ def main(sysArgs=None):
nwGUI = GuiMain()
sys.exit(nwApp.exec_())
return
# END Function main
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+207
View File
@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 270.93333 270.93334"
height="1024"
width="1024">
<defs
id="defs2">
<linearGradient
id="linearGradient2515">
<stop
style="stop-color:#f7f7f7;stop-opacity:1"
offset="0"
id="stop2511" />
<stop
style="stop-color:#e5e5e5;stop-opacity:1"
offset="1"
id="stop2513" />
</linearGradient>
<rect
x="60.484347"
y="205.79962"
width="130.57787"
height="65.785888"
id="rect852" />
<rect
id="rect1474"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<rect
id="rect852-7"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1544"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1570" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1587" />
<filter
id="filter1444"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1434"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1436"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1438"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1440"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1442"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<filter
id="filter1456"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1446"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1448"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1450"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1452"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1454"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<radialGradient
xlink:href="#linearGradient2515"
id="radialGradient2517"
cx="605.42725"
cy="623.97186"
fx="605.42725"
fy="623.97186"
r="410"
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer3"
style="display:inline">
<path
clip-path="none"
transform="scale(0.26458333)"
d="m 192,22 c -44.32,0 -80,35.680002 -80,80 v 820 c 0,44.32002 35.68,80 80,80 h 640 c 44.31999,0 80,-35.67998 80,-80 V 213.44727 L 720.55469,22 Z"
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1481" />
<path
id="path1490"
style="display:inline;fill:#e0e0e0;fill-opacity:1;stroke:#999999;stroke-width:5.29167;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 241.30037,56.474215 190.64713,5.8204593 V 35.307549 c 0,11.726333 9.44034,21.166666 21.16667,21.166666 z" />
<g
aria-label=".nwx"
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
id="text850"
style="font-size:5.64444px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect852);display:inline;fill:#666666;fill-opacity:1">
<path
d="m 63.088865,241.76928 q 0,-1.36426 0.909505,-2.27376 0.930175,-0.93018 2.480467,-0.93018 1.550291,0 2.459796,0.93018 0.909504,0.9095 0.909504,2.27376 0,1.34359 -0.909504,2.27376 -0.909505,0.9095 -2.459796,0.9095 -1.550292,0 -2.480467,-0.9095 -0.909505,-0.93017 -0.909505,-2.27376 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path856" />
<path
d="m 84.75161,226.78312 q -2.542479,0 -3.782712,2.14974 v 15.79231 h -5.973791 v -22.36554 h 5.601721 l 0.206705,2.56314 q 2.397785,-2.97656 6.428544,-2.97656 3.162595,0 5.105627,1.88103 1.943033,1.88102 1.963703,6.55256 v 14.34537 h -5.994461 v -14.26269 q 0,-2.14973 -0.930175,-2.91454 -0.909505,-0.76482 -2.625161,-0.76482 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path858" />
<path
d="m 126.96089,222.35963 -5.68441,22.36554 h -5.00227 l -4.25814,-14.07665 -4.23746,14.07665 h -4.9816 l -5.705078,-22.36554 h 5.767088 l 2.95589,14.22134 4.05143,-14.22134 h 4.32014 l 4.09277,14.20067 2.91455,-14.20067 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path860" />
<path
d="m 134.77436,222.35963 3.65869,6.65592 3.76204,-6.65592 h 6.3872 l -6.36653,10.93472 6.63525,11.43082 h -6.3872 l -4.01009,-7.04866 -3.94808,7.04866 h -6.42854 l 6.63525,-11.43082 -6.36654,-10.93472 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path862" />
</g>
</g>
<g
id="g2509"
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)">
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="rect1406-1"
style="display:inline;fill:#a11212;fill-opacity:1;stroke:#7e1b1b;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1444)"
d="m 145.18984,56.414257 c -44.32,0 -79.999998,35.68 -79.999998,80.000003 v 199.99805 a 279.99999,279.99999 0 0 0 0,0.002 279.99999,279.99999 0 0 0 0,0.0195 v 199.98242 c 0,44.31999 35.679998,80 79.999998,80 44.32,0 80,-35.68001 80,-80 v -200 a 120,120 0 0 1 120,-120 120,120 0 0 1 120,120 v 282.83592 h 160 V 336.41621 a 279.99999,279.99999 0 0 0 -280,-280.000003 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 c -14.4245,-20.75262 -38.42829,-34.29883 -65.72461,-34.29883 z M 376.65078,619.64082 c -0.62174,0.0972 -1.2172,0.22317 -1.7793,0.37696 0.5621,-0.15379 1.15756,-0.27973 1.7793,-0.37696 z m -1.7793,0.37696 c -0.5621,0.15378 -1.09071,0.33534 -1.58008,0.53906 0.48937,-0.20372 1.01798,-0.38528 1.58008,-0.53906 z m 338.85742,-0.37696 c 0.62174,0.0972 1.2172,0.22317 1.7793,0.37696 -0.5621,-0.15379 -1.15756,-0.27973 -1.7793,-0.37696 z m 1.7793,0.37696 c 0.5621,0.15378 1.09071,0.33534 1.58008,0.53906 -0.48938,-0.20372 -1.01798,-0.38528 -1.58008,-0.53906 z" />
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="path897"
style="display:inline;fill:#730d0d;fill-opacity:1;stroke:#590d0d;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1456)"
d="m 465.18984,619.25215 v 0.16406 h -85.58398 c -5.49302,0 -9.91602,2.2329 -9.91602,5.00586 0,0.52294 0.15913,1.02696 0.45117,1.5 h -0.44531 c 0.30916,2.03607 1.24102,3.99791 2.81641,5.57227 l 165.59961,165.49414 c 3.92061,3.91808 10.23371,3.91812 14.15429,0 L 717.86757,631.49434 c 1.57569,-1.57463 2.50725,-3.53578 2.81641,-5.57227 h -0.44531 c 0.29216,-0.47304 0.45117,-0.97706 0.45117,-1.5 0,-2.77296 -4.42304,-5.00586 -9.91602,-5.00586 h -85.58398 v -0.16406 z" />
<rect
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1"
id="rect914"
width="6.7583966"
height="82.482399"
x="-118.27303"
y="1.2969339"
rx="3.3791983"
ry="3.6658845" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h2>Release Notes for 1.0 RC1</h2>
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
few minor tweaks have been made to the GUI.</p>
<p>A number of features and tools are now automatically switched off when there is no project or
document open for those features to act upon. Previously, this was a bit inconsistent, although no
serious bugs have been reported or encountered.</p>
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
are a couple of noticeable changes.</p>
<h3>Typewriter Mode</h3>
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
scroll the document up when the cursor skips to a new line while typing (or down in case of
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
It improves the writing experience as the current active line will stay at the same eye height
level on the screen.</p>
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
top where this feature is activated. These changes makes it more flexible in terms of where the
focus is in the editor. The feature can be controlled from the main Preferences.</p>
<h3>Switching Syntax Theme</h3>
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
Previously, changing the theme would only half-way update the document, header and footer
background and text colours. The new settings would not be fully applied until the application was
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
one you want.</p>
<p>Switching main GUI theme still requires a restart.</p>
<p><i>The full changelog is available
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
</body>
</html>
+1 -1
View File
@@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
background = 255, 255, 255
text = 0, 0, 0
link = 0, 0, 200
headertext = 0, 100, 00
headertext = 0, 100, 0
headertag = 50, 100, 50
emphasis = 150, 110, 30
straightquotes = 200, 0, 0
+25
View File
@@ -0,0 +1,25 @@
[Main]
name = Grey Dark
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 54, 54, 54
text = 200, 200, 200
link = 200, 200, 200
headertext = 225, 225, 225
headertag = 225, 225, 225
emphasis = 200, 200, 200
straightquotes = 200, 200, 200
doublequotes = 200, 200, 200
singlequotes = 200, 200, 200
hidden = 150, 150, 150
keyword = 225, 225, 225
value = 200, 200, 200
spellcheckline = 200, 46, 0
tagerror = 46, 200, 0
replacetag = 225, 225, 225
modifier = 225, 225, 225
+25
View File
@@ -0,0 +1,25 @@
[Main]
name = Grey Light
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 255, 255, 255
text = 20, 20, 20
link = 20, 20, 20
headertext = 0, 0, 0
headertag = 0, 0, 0
emphasis = 20, 20, 20
straightquotes = 20, 20, 20
doublequotes = 20, 20, 20
singlequotes = 20, 20, 20
hidden = 100, 100, 100
keyword = 0, 0, 0
value = 20, 20, 20
spellcheckline = 200, 0, 0
tagerror = 0, 150, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0
+56 -40
View File
@@ -27,12 +27,12 @@
import logging
import configparser
import shutil
import json
import sys
import os
from time import time
from shutil import which
from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
@@ -44,10 +44,11 @@ logger = logging.getLogger(__name__)
class Config:
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
CNF_LIST = 3
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
CNF_S_LST = 3
CNF_I_LST = 4
def __init__(self):
@@ -91,6 +92,7 @@ class Config:
self.guiFont = "" # Defaults to system default font
self.guiFontSize = 11
self.guiScale = 1.0 # Set automatically by Theme class
self.lastNotes = "" # The latest release notes that have been shown
## Sizes
self.winGeometry = [1200, 650]
@@ -210,12 +212,8 @@ class Config:
self.osUnknown = True
# Other System Info
if self.verQtValue >= 50600:
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
else:
self.hostName = "Unknown"
self.kernelVer = "Unknown"
self.hostName = "Unknown"
self.kernelVer = "Unknown"
# Packages
self.hasEnchant = False # The pyenchant package
@@ -320,6 +318,11 @@ class Config:
self.errData.append(str(e))
self.dataPath = None
# Host and Kernel
if self.verQtValue >= 50600:
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Load recent projects cache
self.loadRecentCache()
@@ -380,29 +383,32 @@ class Config:
self.guiFontSize = self._parseLine(
cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
)
self.lastNotes = self._parseLine(
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
)
## Sizes
cnfSec = "Sizes"
self.winGeometry = self._parseLine(
cnfParse, cnfSec, "geometry", self.CNF_LIST, self.winGeometry
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
)
self.treeColWidth = self._parseLine(
cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
)
self.projColWidth = self._parseLine(
cnfParse, cnfSec, "projcols", self.CNF_LIST, self.projColWidth
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
)
self.mainPanePos = self._parseLine(
cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos
cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos
)
self.docPanePos = self._parseLine(
cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos
cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos
)
self.viewPanePos = self._parseLine(
cnfParse, cnfSec, "viewpane", self.CNF_LIST, self.viewPanePos
cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos
)
self.outlnPanePos = self._parseLine(
cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos
cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos
)
self.isFullScreen = self._parseLine(
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
@@ -480,10 +486,10 @@ class Config:
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
)
self.fmtSingleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes
cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes
)
self.fmtDoubleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
)
self.spellTool = self._parseLine(
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
@@ -584,6 +590,7 @@ class Config:
cnfParse.set(cnfSec, "guidark", str(self.guiDark))
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
## Sizes
cnfSec = "Sizes"
@@ -896,23 +903,28 @@ class Config:
# Internal Functions
##
def _unpackList(self, inStr, listLen, listDefault, castTo=int):
"""Unpack a comma separated string of items into a list.
"""
inData = inStr.split(",")
outData = []
for i in range(listLen):
try:
outData.append(castTo(inData[i]))
except Exception:
outData.append(listDefault[i])
return outData
def _packList(self, inData):
"""Pack a list of items into a comma separated string.
"""Pack a list of items into a comma-separated string.
"""
return ", ".join([str(inVal) for inVal in inData])
def _unpackList(self, inStr, listDefault, cnfType):
"""Unpack a comma-separated string of items into a list.
"""
inData = inStr.split(",")
outData = listDefault.copy()
for i in range(min(len(inData), len(listDefault))):
try:
if cnfType == self.CNF_S_LST:
outData[i] = inData[i].strip()
elif cnfType == self.CNF_I_LST:
outData[i] = int(inData[i].strip())
else:
continue
except Exception:
continue
return outData
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
"""Parse a line and return the correct datatype.
"""
@@ -925,18 +937,24 @@ class Config:
return cnfParse.getint(cnfSec, cnfName)
elif cnfType == self.CNF_BOOL:
return cnfParse.getboolean(cnfSec, cnfName)
elif cnfType == self.CNF_LIST:
elif cnfType == self.CNF_I_LST:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST
)
elif cnfType == self.CNF_S_LST:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
)
except ValueError as e:
logger.error("Failed to load value from config file.")
logger.error(str(e))
return cnfDefault
return cnfDefault
def _checkNone(self, checkVal):
"""Convert a string to a none type.
"""Return a NoneType if the value correspomds to None, otherwise
return the value unchanged.
"""
if checkVal is None:
return None
@@ -956,10 +974,8 @@ class Config:
self.hasEnchant = False
logger.debug("Checking package 'pyenchant': Missing")
try:
self.hasAssistant = which("assistant")
except Exception:
self.hasAssistant = False
assistPath = shutil.which("assistant")
self.hasAssistant = assistPath is not None
if self.hasAssistant:
logger.debug("Checking executable 'assistant': Ok")
else:
+2 -1
View File
@@ -168,7 +168,8 @@ class NWDoc():
os.unlink(docPath)
os.rename(docTemp, docPath)
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
if self._theItem is not None:
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
return True
+10 -6
View File
@@ -43,7 +43,7 @@ class NWItem():
self.itemName = ""
self.itemHandle = None
self.itemParent = None
self.itemOrder = None
self.itemOrder = 0
self.itemType = nwItemType.NO_TYPE
self.itemClass = nwItemClass.NO_CLASS
self.itemLayout = nwItemLayout.NO_LAYOUT
@@ -96,20 +96,21 @@ class NWItem():
return False
if "handle" in xItem.attrib:
self.itemHandle = xItem.attrib["handle"]
self.setHandle(xItem.attrib["handle"])
else:
logger.error("XML item entry does not have a handle")
return False
if "parent" in xItem.attrib:
self.itemParent = xItem.attrib["parent"]
self.setParent(xItem.attrib["parent"])
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
retStatus = True
for xValue in xItem:
if xValue.tag == "name":
self.setName(xValue.text)
elif xValue.tag == "order":
self.setOrder(xValue.text)
elif xValue.tag == "type":
self.setType(xValue.text)
elif xValue.tag == "class":
@@ -156,7 +157,10 @@ class NWItem():
def setName(self, theName):
"""Set the item name.
"""
self.itemName = theName.strip()
if isinstance(theName, str):
self.itemName = theName.strip()
else:
self.itemName = ""
return
def setHandle(self, theHandle):
+3 -15
View File
@@ -176,11 +176,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
except Exception as e:
logger.warning(str(e))
return defaultValue
return self.theState[getGroup][getName]
return defaultValue
def getString(self, getGroup, getName, defaultValue):
@@ -189,11 +185,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return str(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return str(self.theState[getGroup][getName])
return defaultValue
def getInt(self, getGroup, getName, defaultValue):
@@ -228,11 +220,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return bool(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return bool(self.theState[getGroup][getName])
return defaultValue
##
+67 -70
View File
@@ -28,12 +28,10 @@
import nw
import logging
import os
import shutil
from lxml import etree
from time import time
from shutil import make_archive, unpack_archive, copyfile
from PyQt5.QtWidgets import QMessageBox
from nw.core.tree import NWTree
from nw.core.item import NWItem
@@ -436,26 +434,15 @@ class NWProject():
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
appVersion = "Unknown"
hexVersion = "0x0"
fileVersion = "Unknown"
self.saveCount = 0
self.autoCount = 0
if "appVersion" in xRoot.attrib:
appVersion = xRoot.attrib["appVersion"]
if "hexVersion" in xRoot.attrib:
hexVersion = xRoot.attrib["hexVersion"]
if "fileVersion" in xRoot.attrib:
fileVersion = xRoot.attrib["fileVersion"]
appVersion = xRoot.attrib.get("appVersion", "Unknown")
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
# The following are deprecated and will be removed
if "saveCount" in xRoot.attrib:
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
if "autoCount" in xRoot.attrib:
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
if "editTime" in xRoot.attrib:
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
# The settings have been moved to the <project> tag
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
logger.verbose("XML root is %s" % nwxRoot)
logger.verbose("File version is %s" % fileVersion)
@@ -483,20 +470,19 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
if fileVersion == "1.0" and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
if fileVersion == "1.0":
msgRes = self.theParent.askQuestion("Old Project Version", (
"The project file and data is created by a novelWriter version "
"lower than 0.7. Do you want to upgrade the project to the "
"most recent format?<br><br>Note that after the upgrade, you "
"cannot open the project with an older version of novelWriter "
"any more, so make sure you have a recent backup."
))
if msgRes != QMessageBox.Yes:
if not msgRes:
self.clearProject()
return False
elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI:
elif fileVersion != "1.1" and fileVersion != "1.2":
self.makeAlert((
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
@@ -510,9 +496,8 @@ class NWProject():
# Check novelWriter Version
# =========================
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Version Conflict", (
if int(hexVersion, 16) > int(nw.__hexversion__, 16):
msgRes = self.theParent.askQuestion("Version Conflict", (
"This project was saved by a newer version of novelWriter, version %s. "
"This is version %s. If you continue to open the project, some attributes "
"and settings may not be preserved, but the overall project should be fine. "
@@ -520,7 +505,7 @@ class NWProject():
) % (
appVersion, nw.__version__
))
if msgRes != QMessageBox.Yes:
if not msgRes:
self.clearProject()
return False
@@ -835,15 +820,15 @@ class NWProject():
try:
self._clearLockFile()
make_archive(baseName, "zip", self.projPath, ".")
shutil.make_archive(baseName, "zip", self.projPath, ".")
self._writeLockFile()
logger.info("Backup written to: %s" % archName)
if doNotify:
self.theParent.makeAlert(
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
nwAlert.INFO
)
else:
logger.info("Backup written to: %s" % archName)
except Exception as e:
self.theParent.makeAlert(
["Could not write backup archive.", str(e)],
@@ -874,7 +859,7 @@ class NWProject():
self.setProjectPath(projPath, newProject=True)
try:
unpack_archive(pkgSample, projPath)
shutil.unpack_archive(pkgSample, projPath)
isSuccess = True
except Exception as e:
self.makeAlert(
@@ -887,14 +872,14 @@ class NWProject():
try:
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
copyfile(srcProj, dstProj)
shutil.copyfile(srcProj, dstProj)
srcContent = os.path.join(srcSample, "content")
dstContent = os.path.join(projPath, "content")
for srcFile in os.listdir(srcContent):
srcDoc = os.path.join(srcContent, srcFile)
dstDoc = os.path.join(dstContent, srcFile)
copyfile(srcDoc, dstDoc)
shutil.copyfile(srcDoc, dstDoc)
isSuccess = True
@@ -998,11 +983,15 @@ class NWProject():
"You must set a valid backup path in preferences to use "
"the automatic project backup feature."
), nwAlert.WARN)
return False
if self.projName == "":
self.theParent.makeAlert((
"You must set a valid project name in project settings to "
"use the automatic project backup feature."
), nwAlert.WARN)
return False
return True
def setSpellCheck(self, theMode):
@@ -1011,12 +1000,15 @@ class NWProject():
if self.spellCheck != theMode:
self.spellCheck = theMode
self.setProjectChanged(True)
return True
return self.spellCheck
def setSpellLang(self, theLang):
"""Set the project-specific spell check language.
"""
self.projLang = checkString(theLang, None, True)
theLang = checkString(theLang, None, True)
if self.projLang != theLang:
self.projLang = theLang
self.setProjectChanged(True)
return True
def setAutoOutline(self, theMode):
@@ -1025,7 +1017,7 @@ class NWProject():
if self.autoOutline != theMode:
self.autoOutline = theMode
self.setProjectChanged(True)
return True
return self.autoOutline
def setTreeOrder(self, newOrder):
"""A list representing the linear/flattened order of project
@@ -1072,7 +1064,7 @@ class NWProject():
if nwItem.itemStatus in replaceMap.keys():
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return
return True
def setImportColours(self, newCols):
"""Update the list of note file importance flags. Also iterate
@@ -1084,14 +1076,15 @@ class NWProject():
if nwItem.itemStatus in replaceMap.keys():
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return
return True
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary. This replaces the entire
dictionary, so alterations have to be made in a copy.
"""
self.autoReplace = autoReplace
return
self.setProjectChanged(True)
return True
def setTitleFormat(self, titleFormat):
"""Set the formatting of titles in the project.
@@ -1099,7 +1092,7 @@ class NWProject():
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
return
return True
def setProjectChanged(self, bValue):
"""Toggle the project changed flag, and propagate the
@@ -1132,13 +1125,11 @@ class NWProject():
sentItems = []
iterItems = self.projTree.handles()
n = 0
nMax = len(iterItems)
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
tItem = self.projTree[tHandle]
n += 1
if n > 10000:
return # Just in case
if tItem is None:
# Technically a bug since treeOrder is built from the
# same data as projTree
@@ -1154,10 +1145,11 @@ class NWProject():
yield tItem
elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add
# it again to the end
# it again to the end, but make sure this doesn't get
# out hand, so we cap at 10000 items
logger.warning("Item %s found before its parent" % tHandle)
iterItems.append(tHandle)
nMax = len(iterItems)
nMax = min(len(iterItems), 10000)
else:
# Item is orphaned
logger.error("Item %s has no parent in current tree" % tHandle)
@@ -1196,13 +1188,12 @@ class NWProject():
if not os.path.isfile(lockFile):
return []
theLines = []
try:
with open(lockFile, mode="r", encoding="utf8") as inFile:
theData = inFile.read()
theLines = theData.splitlines()
if len(theLines) == 4:
return theLines
else:
if len(theLines) != 4:
return ["ERROR"]
except Exception as e:
@@ -1210,7 +1201,7 @@ class NWProject():
logger.error(str(e))
return ["ERROR"]
return ["ERROR"]
return theLines
def _writeLockFile(self):
"""Writes a lock file to the project folder.
@@ -1243,13 +1234,12 @@ class NWProject():
if os.path.isfile(lockFile):
try:
os.unlink(lockFile)
return True
except Exception as e:
logger.error("Failed to remove project lockfile")
logger.error(str(e))
return False
return None
return True
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
@@ -1292,7 +1282,7 @@ class NWProject():
back into the project tree.
"""
if self.projPath is None:
return
return False
# Then check the files in the data folder
logger.debug("Checking files in project content folder")
@@ -1352,7 +1342,7 @@ class NWProject():
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, None, orphItem)
return
return True
def _appendSessionStats(self):
"""Append session statistics to the sessions log file.
@@ -1363,21 +1353,27 @@ class NWProject():
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = os.path.isfile(sessionFile)
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
if not isFile:
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
try:
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
if not isFile:
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
))
outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
except Exception as e:
logger.error("Failed to write session stats file")
logger.error(str(e))
return False
return True
@@ -1490,7 +1486,8 @@ class NWProject():
os.unlink(rmFile)
except Exception as e:
logger.error(str(e))
return False
return
return True
# END Class NWProject
+18 -14
View File
@@ -28,8 +28,7 @@
import nw
import logging
import os
from difflib import get_close_matches
import difflib
from nw.constants import nwConst, isoLanguage
@@ -70,14 +69,16 @@ class NWSpellCheck():
"""
if self.projectDict is not None and newWord not in self.projDict:
newWord = newWord.strip()
self.projDict.append(newWord)
try:
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.append(newWord)
except Exception as e:
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
logger.error(str(e))
return
return False
return True
return False
def listDictionaries(self):
"""Dummy function.
@@ -109,9 +110,12 @@ class NWSpellCheck():
"""
self.projDict = []
if projectDict is not None:
self.projectDict = projectDict
if not os.path.isfile(projectDict):
return
self.projectDict = None
return False
else:
self.projectDict = projectDict
try:
logger.debug("Loading project word list")
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
@@ -123,7 +127,9 @@ class NWSpellCheck():
except Exception as e:
logger.error("Failed to load project word list")
logger.error(str(e))
return
return False
return True
# END Class NWSpellCheck
@@ -287,7 +293,7 @@ class NWSpellSimple(NWSpellCheck):
if len(theWord) == 0:
return []
theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theOptions = []
for aWord in theMatches:
if len(aWord) == 0:
@@ -314,14 +320,12 @@ class NWSpellSimple(NWSpellCheck):
retList = []
for dictFile in os.listdir(self.mainConf.dictPath):
theBits = os.path.splitext(dictFile)
if len(theBits) != 2:
continue
if theBits[1] != ".dict":
fRoot, fExt = os.path.splitext(dictFile)
if fExt != ".dict":
continue
spName = "%s [%s]" % (self.expandLanguage(theBits[0]), nwConst.SP_INTERNAL)
retList.append((theBits[0], spName))
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
retList.append((fRoot, spName))
return retList
+6 -13
View File
@@ -79,6 +79,7 @@ class NWStatus():
theStatus = checkInt(theStatus, 0, False)
if theStatus >= 0 and theStatus < self._theLength:
return self._theLabels[theStatus]
return self._theLabels[0]
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
@@ -108,7 +109,8 @@ class NWStatus():
return
def countEntry(self, theLabel):
"""Lookup the usage count of a given entry.
"""Increment the counter for a given label. This should be used
together with resetCounts in a loop over project items.
"""
theIndex = self.lookupEntry(theLabel)
if theIndex is not None:
@@ -136,18 +138,9 @@ class NWStatus():
for xChild in xParent:
theLabels.append(xChild.text)
if "red" in xChild.attrib:
cR = checkInt(xChild.attrib["red"], 0, False)
else:
cR = 0
if "green" in xChild.attrib:
cG = checkInt(xChild.attrib["green"], 0, False)
else:
cG = 0
if "blue" in xChild.attrib:
cB = checkInt(xChild.attrib["blue"], 0, False)
else:
cB = 0
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
theColours.append((cR, cG, cB))
if len(theLabels) > 0:
+11 -9
View File
@@ -68,16 +68,16 @@ class ToHtml(Tokenizer):
# Setters
##
def setPreview(self, forPreview, doComments, doSynopsis):
def setPreview(self, doComments, doSynopsis):
"""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.doSynopsis = doSynopsis
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
return
def setStyles(self, cssStyles):
@@ -141,11 +141,13 @@ class ToHtml(Tokenizer):
# For novel files for export, we bump the titles one level
# up as this is more useful for printing and word processor
# imports.
h1 = "h1 class='title'"
h1Cl = " class='title'"
h1 = "h1"
h2 = "h1"
h3 = "h2"
h4 = "h3"
else:
h1Cl = ""
h1 = "h1"
h2 = "h2"
h3 = "h3"
@@ -193,7 +195,7 @@ class ToHtml(Tokenizer):
else:
aNm = ""
# Process TextType
# Process Text Type
if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
@@ -214,7 +216,7 @@ class ToHtml(Tokenizer):
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
+8 -7
View File
@@ -180,7 +180,7 @@ class Tokenizer():
##
def addRootHeading(self, theHandle):
"""Add a heading at the start if a new root folder.
"""Add a heading at the start of a new root folder.
"""
theItem = self.theProject.projTree[theHandle]
if theItem is None:
@@ -205,7 +205,7 @@ class Tokenizer():
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if self.theItem is None:
return
return False
if theText is not None:
# If the text is set, just use that
@@ -234,7 +234,7 @@ class Tokenizer():
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
return
return True
def getResult(self):
"""Return the result from the conversion.
@@ -244,6 +244,8 @@ class Tokenizer():
def getResultSize(self):
"""Return the size of the result from the conversion.
"""
if self.theResult is None:
return 0
return len(self.theResult)
def getFilteredMarkdown(self):
@@ -445,7 +447,7 @@ class Tokenizer():
"""
# No special header formatting for notes and no-layout files
if self.isNone or self.isNote:
return
return False
# For novel files, we need to handle chapter numbering, scene
# numbering, and scene breaks
@@ -479,8 +481,7 @@ class Tokenizer():
if self.isUnNum:
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
elif tToken[2].startswith("*"):
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
tTemp = tTemp[1:].lstrip()
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
else:
self.numChapter += 1
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
@@ -663,7 +664,7 @@ class Tokenizer():
self.A_LEFT
)
return
return True
##
# Internal Functions
+15 -5
View File
@@ -141,6 +141,15 @@ def _numberToWordEN(numVal):
tenWord = ""
hunWord = ""
if not isinstance(numVal, int):
return "[NaN]"
if numVal < 0:
return "[Negative]"
if numVal > 999:
return "[Out of Range]"
if numVal == 0:
return "Zero"
@@ -166,19 +175,20 @@ def _numberToWordEN(numVal):
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
}
retVale = ""
hunWord = theHundreds.get(hunVal, "")
if tenVal == 10:
oneWord = theTeens.get(oneVal, "")
return f"{hunWord} {oneWord}".strip()
retVale = f"{hunWord} {oneWord}".strip()
else:
oneWord = theOnes.get(oneVal, "")
if tenVal == 0:
return f"{hunWord} {oneWord}".strip()
retVale = f"{hunWord} {oneWord}".strip()
else:
tenWord = theTens.get(tenVal, "")
if oneVal == 0:
return f"{hunWord} {tenWord}".strip()
retVale = f"{hunWord} {tenWord}".strip()
else:
return f"{hunWord} {tenWord}-{oneWord}".strip()
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
return ""
return retVale
+16 -7
View File
@@ -48,8 +48,10 @@ class NWTree():
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed
self._handleSeed = None # Used for generating handles for testing
return
@@ -83,14 +85,15 @@ class NWTree():
if tHandle is None:
tHandle = self._makeHandle()
if tHandle in self._projTree:
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
return False
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item %s is a root item" % str(tHandle))
self._treeRoots.append(tHandle)
@@ -104,10 +107,13 @@ class NWTree():
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
self._setTreeChanged(True)
return
return True
def packXML(self, xParent):
"""Pack the content of the tree into the provided XML object. In
@@ -176,8 +182,9 @@ class NWTree():
except Exception as e:
logger.error(str(e))
return False
return
return True
def sumWords(self):
"""Loops over all entries and adds up the word counts.
@@ -244,6 +251,8 @@ class NWTree():
return True
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return False
return True
@@ -397,7 +406,7 @@ class NWTree():
del self._projTree[tHandle]
else:
logger.warning("Failed to delete item %s: item not found" % tHandle)
return False
return
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
@@ -408,7 +417,7 @@ class NWTree():
self._setTreeChanged(True)
return True
return
def __contains__(self, tHandle):
"""Checks if a handle exists in the tree.
+3 -8
View File
@@ -135,10 +135,9 @@ class NWErrorMessage(QDialog):
# END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace, testMode=False):
def exceptionHandler(exType, exValue, exTrace):
"""Function to catch unhandled global exceptions.
"""
import nw
import logging
from traceback import print_tb
from PyQt5.QtWidgets import qApp
@@ -160,8 +159,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
errMsg = NWErrorMessage(nwGUI)
errMsg.setMessage(exType, exValue, exTrace)
if nw.CONFIG.showGUI:
errMsg.exec_()
errMsg.exec_()
try:
# Try a controlled shudown
@@ -173,10 +171,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
logger.critical("Could not close the project before exiting")
logger.critical(str(e))
if testMode:
return errMsg.msgBody.toPlainText()
else:
qApp.exit(1)
qApp.exit(1)
except Exception as e:
logger.critical(str(e))
+40 -9
View File
@@ -32,8 +32,9 @@ import os
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
QTextBrowser, QLabel
)
@@ -80,6 +81,10 @@ class GuiAbout(QDialog):
self.pageAbout.setOpenExternalLinks(True)
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
self.pageNotes = QTextBrowser()
self.pageNotes.setOpenExternalLinks(True)
self.pageNotes.document().setDocumentMargin(self.mainConf.pxInt(16))
self.pageLicense = QTextBrowser()
self.pageLicense.setOpenExternalLinks(True)
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
@@ -87,6 +92,7 @@ class GuiAbout(QDialog):
# Main Tab Area
self.tabBox = QTabWidget()
self.tabBox.addTab(self.pageAbout, "About")
self.tabBox.addTab(self.pageNotes, "Release")
self.tabBox.addTab(self.pageLicense, "License")
self.innerBox.addWidget(self.tabBox)
@@ -98,14 +104,27 @@ class GuiAbout(QDialog):
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self._setStyleSheet()
self._fillAboutPage()
self._fillLicensePage()
logger.debug("GuiAbout initialisation complete")
return
def populateGUI(self):
"""Populate tabs with text.
"""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._setStyleSheet()
self._fillAboutPage()
self._fillNotesPage()
self._fillLicensePage()
qApp.restoreOverrideCursor()
return
def showReleaseNotes(self):
"""Show the release notes.
"""
self.tabBox.setCurrentWidget(self.pageNotes)
return
##
# Internal Functions
##
@@ -121,14 +140,14 @@ class GuiAbout(QDialog):
"<p>novelWriter 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>novelWriter is free software: you can redistribute it and/or "
"<p>novelWriter is free software: you can redistribute it and "
"modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of "
"the License, or (at your option) any later version.</p>"
"<p>novelWriter 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 "
"<p>See the License tab for the full license text, or visit the GNU website "
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"for more details.</p>"
"<h3>Credits</h3>"
@@ -192,11 +211,22 @@ class GuiAbout(QDialog):
return
def _fillNotesPage(self):
"""Load the content for the Release Notes page.
"""
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.pageNotes.setHtml(helpText)
else:
self.pageNotes.setHtml("Error loading release notes text ...")
return
def _fillLicensePage(self):
"""Load the content for the License page.
"""
docName = "gplv3_%s.htm" % self.mainConf.guiLang
docPath = os.path.join(self.mainConf.assetPath, "text", docName)
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
@@ -221,6 +251,7 @@ class GuiAbout(QDialog):
hColB = self.theParent.theTheme.colHead[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return
+4 -3
View File
@@ -262,10 +262,10 @@ class GuiDocEditor(QTextEdit):
# If we have a document open, we should reload it in case the
# font changed, otherwise we just clear the editor entirely,
# which makes it read only.
if self.theHandle is not None:
self.redrawText()
else:
if self.theHandle is None:
self.clearEditor()
else:
self.redrawText()
return True
@@ -355,6 +355,7 @@ class GuiDocEditor(QTextEdit):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.updateDocMargins()
return
def replaceText(self, theText):
+3 -2
View File
@@ -149,7 +149,7 @@ class GuiDocViewer(QTextBrowser):
# If we have a document open, we should reload it in case the font changed
if self.theHandle is not None:
self.redrawText()
self.reloadText()
return True
@@ -169,7 +169,7 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
aDoc.setLinkHeaders(True)
# Be extra careful here to prevent crashes when first opening a
@@ -233,6 +233,7 @@ class GuiDocViewer(QTextBrowser):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.updateDocMargins()
return
def loadFromTag(self, theTag):
+26 -11
View File
@@ -598,8 +598,8 @@ class GuiMainMenu(QMenuBar):
# Insert > Separator
self.insertMenu.addSeparator()
# Insert > Keywords and Tags
self.mInsKeywords = self.insertMenu.addMenu("Keywords and Tags")
# Insert > Tags and References
self.mInsKeywords = self.insertMenu.addMenu("Tags and References")
self.mInsKWItems = {}
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
@@ -908,7 +908,7 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addAction(self.aHelpLoc)
self.aHelpWeb = QAction("Documentation (Online)", self)
self.aHelpWeb.setStatusTip("View online documentation")
self.aHelpWeb.setStatusTip("View online documentation at %s" % nw.__docurl__)
self.aHelpWeb.triggered.connect(lambda: self._openWebsite(nw.__docurl__))
if self.mainConf.hasHelp and self.mainConf.hasAssistant:
self.aHelpWeb.setShortcut("Shift+F1")
@@ -916,18 +916,33 @@ class GuiMainMenu(QMenuBar):
self.aHelpWeb.setShortcuts(["F1", "Shift+F1"])
self.helpMenu.addAction(self.aHelpWeb)
# Document > Go to Website
self.aWebsite = QAction("Open the novelWriter Website", self)
self.aWebsite.setStatusTip("View the main website")
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
self.helpMenu.addAction(self.aWebsite)
# Help > Separator
self.helpMenu.addSeparator()
# Document > Report Issue
self.aIssue = QAction("Report an Issue", self)
self.aIssue.setStatusTip("Report a bug or issue on GitHub")
# Document > Report an Issue
self.aIssue = QAction("Report an Issue (GitHub)", self)
self.aIssue.setStatusTip("Report a bug or issue on GitHub at %s" % nw.__issuesurl__)
self.aIssue.triggered.connect(lambda: self._openWebsite(nw.__issuesurl__))
self.helpMenu.addAction(self.aIssue)
# Document > Ask a Question
self.aQuestion = QAction("Ask a Question (GitHub)", self)
self.aQuestion.setStatusTip("Ask a question on GitHub at %s" % nw.__helpurl__)
self.aQuestion.triggered.connect(lambda: self._openWebsite(nw.__helpurl__))
self.helpMenu.addAction(self.aQuestion)
# Document > Latest Release
self.aRelease = QAction("Latest Release (GitHub)", self)
self.aRelease.setStatusTip("Open the Releases page on GitHub at %s" % nw.__releaseurl__)
self.aRelease.triggered.connect(lambda: self._openWebsite(nw.__releaseurl__))
self.helpMenu.addAction(self.aRelease)
# Document > Main Website
self.aWebsite = QAction("The novelWriter Website", self)
self.aWebsite.setStatusTip("Open the novelWriter website at %s" % nw.__url__)
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
self.helpMenu.addAction(self.aWebsite)
return
# END Class GuiMainMenu
+47 -35
View File
@@ -227,7 +227,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Show full path in document header",
self.showFullPath,
"Shows the document title and parent folder names."
"Add the parent folder names to the header."
)
self.hideVScroll = QSwitch()
@@ -235,7 +235,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Hide vertical scroll bars in main windows",
self.hideVScroll,
"Scrolling with mouse wheel and keys only."
"Scrolling available with mouse wheel and keys only."
)
self.hideHScroll = QSwitch()
@@ -243,7 +243,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Hide horizontal scroll bars in main windows",
self.hideHScroll,
"Scrolling with mouse wheel and keys only."
"Scrolling available with mouse wheel and keys only."
)
return
@@ -325,8 +325,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.autoSaveDoc.setSingleStep(1)
self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc)
self.backupPathRow = self.mainForm.addRow(
"Save interval for the currently open document",
"Save document interval",
self.autoSaveDoc,
"How often the open document is automatically saved.",
theUnit="seconds"
)
@@ -337,8 +338,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.autoSaveProj.setSingleStep(1)
self.autoSaveProj.setValue(self.mainConf.autoSaveProj)
self.backupPathRow = self.mainForm.addRow(
"Save interval for the currently open project",
"Save project interval",
self.autoSaveProj,
"How often the open project is automatically saved.",
theUnit="seconds"
)
@@ -361,9 +363,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.backupOnClose.setChecked(self.mainConf.backupOnClose)
self.backupOnClose.toggled.connect(self._toggledBackupOnClose)
self.mainForm.addRow(
"Run backup when closing project",
"Run backup when the project is closed",
self.backupOnClose,
"This option can be overridden in project settings."
"Can be overridden for individual projects in project settings."
)
## Ask before backup
@@ -373,7 +375,8 @@ class GuiConfigEditProjectsTab(QWidget):
self.askBeforeBackup.setEnabled(self.mainConf.backupOnClose)
self.mainForm.addRow(
"Ask before running backup",
self.askBeforeBackup
self.askBeforeBackup,
"Disabling this will cause backups to run in the background."
)
return
@@ -426,7 +429,7 @@ class GuiConfigEditProjectsTab(QWidget):
def _toggledBackupOnClose(self, theState):
"""Enable or disable switch that depends on the backup on close
switch,
switch.
"""
self.askBeforeBackup.setEnabled(theState)
return
@@ -449,7 +452,7 @@ class GuiConfigEditLayoutTab(QWidget):
# Text Style
# ==========
self.mainForm.addGroupLabel("Text Style")
self.mainForm.addGroupLabel("Document Text Style")
## Font Family
self.textStyleFont = QLineEdit()
@@ -475,12 +478,13 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Font size",
self.textStyleSize,
"Font size for the document editor and viewer.",
theUnit = "pt"
)
# Text Flow
# =========
self.mainForm.addGroupLabel("Text Flow")
self.mainForm.addGroupLabel("Document Text Flow")
## Max Text Width in Normal Mode
self.textFlowMax = QSpinBox(self)
@@ -491,6 +495,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Maximum text width in \"Normal Mode\"",
self.textFlowMax,
"Horizontal margins are scaled automatically.",
theUnit="px"
)
@@ -503,6 +508,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Maximum text width in \"Focus Mode\"",
self.focusDocWidth,
"Horizontal margins are scaled automatically.",
theUnit="px"
)
@@ -512,7 +518,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Disable maximum text width in \"Normal Mode\"",
self.textFlowFixed,
"If disabled, minimum text width is defined by the margin setting."
"If disabled, minimum text width is defined by the margin."
)
## Focus Mode Footer
@@ -520,7 +526,8 @@ class GuiConfigEditLayoutTab(QWidget):
self.hideFocusFooter.setChecked(self.mainConf.hideFocusFooter)
self.mainForm.addRow(
"Hide document footer in \"Focus Mode\"",
self.hideFocusFooter
self.hideFocusFooter,
"Hide the information bar at the bottom of the document."
)
## Justify Text
@@ -528,7 +535,8 @@ class GuiConfigEditLayoutTab(QWidget):
self.textJustify.setChecked(self.mainConf.textFixedW)
self.mainForm.addRow(
"Justify the text margins in editor and viewer",
self.textJustify
self.textJustify,
"Lay out text with straight edges in the editor and viewer."
)
## Document Margins
@@ -538,9 +546,9 @@ class GuiConfigEditLayoutTab(QWidget):
self.textMargin.setSingleStep(1)
self.textMargin.setValue(self.mainConf.textMargin)
self.mainForm.addRow(
"Document text margin",
"Text margin",
self.textMargin,
"If max width is enabled, this is the minimum margin.",
"If maximum width is set, this becomes the minimum margin.",
theUnit="px"
)
@@ -551,8 +559,9 @@ class GuiConfigEditLayoutTab(QWidget):
self.tabWidth.setSingleStep(1)
self.tabWidth.setValue(self.mainConf.tabWidth)
self.mainForm.addRow(
"Document tab width",
"Tab width",
self.tabWidth,
"The width of a tab key press in the editor and viewer.",
theUnit="px"
)
@@ -566,7 +575,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Scroll past end of the document",
self.scrollPastEnd,
"Allows scrolling until last line is at the top."
"Allow scrolling until the last line is centred in the editor."
)
## Typewriter Scrolling
@@ -575,7 +584,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Typewriter style scrolling when you type",
self.autoScroll,
"Tries to keep the cursor at a fixed vertical position."
"Try to keep the cursor at a fixed vertical position."
)
## Font Size
@@ -678,7 +687,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Highlight theme",
self.selectSyntax,
""
"Colour theme to apply to the editor and viewer."
)
self.highlightQuotes = QSwitch()
@@ -686,7 +695,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Highlight text wrapped in quotes",
self.highlightQuotes,
helpText="Applies to single, double and straight quotes."
"Applies to single, double and straight quotes."
)
self.highlightEmph = QSwitch()
@@ -694,7 +703,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Add highlight colour to emphasised text",
self.highlightEmph,
helpText="Applies to emphasis, strong and strikethrough."
"Applies to emphasis, strong and strikethrough."
)
# Spell Checking
@@ -724,7 +733,8 @@ class GuiConfigEditEditingTab(QWidget):
)
self.mainForm.addRow(
"Spell check language",
self.spellLangList
self.spellLangList,
"Available languages are determined by your system."
)
## Big Document Size Limit
@@ -736,7 +746,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Big document limit",
self.bigDocLimit,
"Disables full spell checking over the size limit.",
"Full spell checking is disabled above this limit.",
theUnit="kB"
)
@@ -749,7 +759,8 @@ class GuiConfigEditEditingTab(QWidget):
self.showTabsNSpaces.setChecked(self.mainConf.showTabsNSpaces)
self.mainForm.addRow(
"Show tabs and spaces",
self.showTabsNSpaces
self.showTabsNSpaces,
"Add symbols to indicate tabs and spaces in the editor."
)
## Show Line Endings
@@ -757,7 +768,8 @@ class GuiConfigEditEditingTab(QWidget):
self.showLineEndings.setChecked(self.mainConf.showLineEndings)
self.mainForm.addRow(
"Show line endings",
self.showLineEndings
self.showLineEndings,
"Add a symbol to indicate line endings in the editor."
)
return
@@ -866,7 +878,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace text as you type",
self.autoReplaceMain,
"Apply formatting to word under cursor if no selection is made."
"Allow the editor to replace symbols as you type."
)
# Auto-Replace
@@ -880,7 +892,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace single quotes",
self.autoReplaceSQ,
"The feature will try to guess opening or closing single quote."
"Try to guess which is an opening or a closing single quote."
)
## Auto-Replace Double Quotes
@@ -890,7 +902,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace double quotes",
self.autoReplaceDQ,
"The feature will try to guess opening or closing quote quote."
"Try to guess which is an opening or a closing double quote."
)
## Auto-Replace Hyphens
@@ -900,7 +912,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace dashes",
self.autoReplaceDash,
"Auto-replace double and triple hyphens with short and long dash."
"Double and triple hyphens become short and long dashes."
)
## Auto-Replace Dots
@@ -910,7 +922,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace dots",
self.autoReplaceDots,
"Auto-replace three dots with ellipsis."
"Three consecutive dots becomes ellipsis."
)
# Quotation Style
@@ -934,7 +946,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Single quote open style",
self.quoteSym["SO"],
"Auto-replaces apostrophe before words.",
"The symbol to use for a leading single quote.",
theButton=self.btnSingleStyleO
)
@@ -950,7 +962,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Single quote close style",
self.quoteSym["SC"],
"Auto-replaces apostrophe after words.",
"The symbol to use for a trailing single quote.",
theButton=self.btnSingleStyleC
)
@@ -967,7 +979,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Double quote open style",
self.quoteSym["DO"],
"Auto-replaces straight quotes before words.",
"The symbol to use for a leading double quote.",
theButton=self.btnDoubleStyleO
)
@@ -983,7 +995,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Double quote close style",
self.quoteSym["DC"],
"Auto-replaces straight quotes after words.",
"The symbol to use for a trailing double quote.",
theButton=self.btnDoubleStyleC
)
+47 -43
View File
@@ -286,36 +286,36 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
if hasFocus and self.theParent.hasProject:
if qApp.focusWidget() != self and self.mainConf.showGUI:
return False
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
pItem = tItem.parent()
if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = self.takeTopLevelItem(tIndex)
self.insertTopLevelItem(nIndex, cItem)
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
if tItem is None:
return False
else:
tIndex = pItem.indexOfChild(tItem)
nChild = pItem.childCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = pItem.takeChild(tIndex)
pItem.insertChild(nIndex, cItem)
self.clearSelection()
cItem.setSelected(True)
self._setTreeChanged(True)
pItem = tItem.parent()
if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = self.takeTopLevelItem(tIndex)
self.insertTopLevelItem(nIndex, cItem)
else:
return False
tIndex = pItem.indexOfChild(tItem)
nChild = pItem.childCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = pItem.takeChild(tIndex)
pItem.insertChild(nIndex, cItem)
self.clearSelection()
cItem.setSelected(True)
self._setTreeChanged(True)
return True
@@ -431,7 +431,7 @@ class GuiProjectTree(QTreeWidget):
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
if nwItemS is None:
if trItemS is None or nwItemS is None:
return False
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
@@ -582,18 +582,23 @@ class GuiProjectTree(QTreeWidget):
properly reported to the function.
"""
tItem = self._getTreeItem(tHandle)
if tItem is not None:
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
pItem = tItem.parent()
if pItem is not None:
pCount = 0
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if tItem is None:
return
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
pItem = tItem.parent()
if pItem is None:
return
pCount = 0
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
return
@@ -1024,12 +1029,12 @@ class GuiProjectTreeMenu(QMenu):
return
def filterActions(self, theItem):
"""Update item settings from the nwItem.
"""Filter the menu entries available based on the properties of
the item the menu was activated on.
"""
self.theItem = theItem
theRoot = self.theTree.theProject.projTree.getRootItem(theItem.itemHandle)
if theItem is None or theRoot is None:
if theItem is None:
logger.error("Failed to extract information to build tree context menu")
return False
@@ -1038,14 +1043,13 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
isOrph = isFile and theItem.itemParent is None
showOpen = isFile
showView = isFile
showEdit = not isTrash and not isOrph
showExport = isFile
showNewFile = not (isTrash or inTrash or isOrph or isArch)
showNewFile = not (isTrash or inTrash or isOrph)
showNewFolder = not (isTrash or inTrash or isOrph)
showDelete = not isTrash
showEmpty = isTrash
+7 -7
View File
@@ -281,11 +281,11 @@ class GuiTheme:
cnfSec = "Main"
if confParser.has_section(cnfSec):
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "N/A")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
## Palette
@@ -638,10 +638,10 @@ class GuiIcons:
if confParser.has_section(cnfSec):
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
## Palette
+26 -6
View File
@@ -81,14 +81,14 @@ class GuiMain(QMainWindow):
# Core Classes
# ============
# Core Classes and settings
# Core Classes and Settings
self.theTheme = GuiTheme(self)
self.theProject = NWProject(self)
self.theIndex = NWIndex(self.theProject, self)
self.hasProject = False
self.isFocusMode = False
# Prepare main window
# Prepare Main Window
self.resize(*self.mainConf.getWinSize())
self._setWindowTitle()
self.setWindowIcon(QIcon(self.mainConf.appIcon))
@@ -238,6 +238,12 @@ class GuiMain(QMainWindow):
if self.mainConf.showGUI:
self.showProjectLoadDialog()
# Show the latest release notes, if they haven't been shown before
if self.mainConf.lastNotes != nw.__version__:
if self.mainConf.showGUI:
self.showAboutNWDialog(showNotes=True)
self.mainConf.lastNotes = nw.__version__
logger.debug("novelWriter is ready ...")
self.setStatus("novelWriter is ready ...")
@@ -915,11 +921,18 @@ class GuiMain(QMainWindow):
return
def showAboutNWDialog(self):
def showAboutNWDialog(self, showNotes=False):
"""Show the about dialog for novelWriter.
"""
dlgAbout = GuiAbout(self)
dlgAbout.exec_()
dlgAbout.setModal(True)
dlgAbout.show()
qApp.processEvents()
dlgAbout.populateGUI()
if showNotes:
dlgAbout.showReleaseNotes()
return
def showAboutQtDialog(self):
@@ -969,6 +982,13 @@ class GuiMain(QMainWindow):
return
def askQuestion(self, theTitle, theQuestion):
"""Ask the user a Yes/No question.
"""
msgBox = QMessageBox()
msgRes = msgBox.question(self, theTitle, theQuestion)
return msgRes == QMessageBox.Yes
def reportConfErr(self):
"""Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches
@@ -1060,10 +1080,10 @@ class GuiMain(QMainWindow):
self.isFocusMode = not self.isFocusMode
if self.isFocusMode:
logger.debug("Activating Focus mode")
logger.debug("Activating Focus Mode")
self.tabWidget.setCurrentWidget(self.splitDocs)
else:
logger.debug("Deactivating Focus mode")
logger.debug("Deactivating Focus Mode")
isVisible = not self.isFocusMode
self.treePane.setVisible(isVisible)