Merge branch 'main' into i18n-de_DE-created
@@ -27,7 +27,6 @@ import sys
|
||||
import getopt
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||
|
||||
from novelwriter.error import exceptionHandler, logException
|
||||
@@ -60,9 +59,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.7-beta1"
|
||||
__hexversion__ = "0x010700b1"
|
||||
__date__ = "2022-05-17"
|
||||
__version__ = "2.0-rc2"
|
||||
__hexversion__ = "0x020000c2"
|
||||
__date__ = "2022-11-13"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
@@ -72,32 +71,6 @@ __helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
|
||||
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||
|
||||
##
|
||||
# Logging
|
||||
# =========
|
||||
# Standard used for logging levels in novelWriter:
|
||||
# CRITICAL Use for errors that result in termination of the program
|
||||
# ERROR Use when an action fails, but execution continues
|
||||
# WARNING When something unexpected, but non-critical happens
|
||||
# INFO Any useful user information like open, save, exit initiated
|
||||
# ----------- SPAM Threshold : Output above should be minimal -----------------
|
||||
# DEBUG Use for descriptions of main program flow
|
||||
# VERBOSE Use for outputting values and program flow details
|
||||
##
|
||||
|
||||
# Add verbose logging level
|
||||
VERBOSE = 5
|
||||
logging.addLevelName(VERBOSE, "VERBOSE")
|
||||
|
||||
|
||||
def logVerbose(self, message, *args, **kws):
|
||||
if self.isEnabledFor(VERBOSE):
|
||||
self._log(VERBOSE, message, args, **kws)
|
||||
|
||||
|
||||
logging.Logger.verbose = logVerbose
|
||||
|
||||
# Initiating logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -122,7 +95,6 @@ def main(sysArgs=None):
|
||||
"version",
|
||||
"info",
|
||||
"debug",
|
||||
"verbose",
|
||||
"style=",
|
||||
"config=",
|
||||
"data=",
|
||||
@@ -143,7 +115,6 @@ def main(sysArgs=None):
|
||||
" -v, --version Print program version and exit.\n"
|
||||
" --info Print additional runtime information.\n"
|
||||
" --debug Print debug output. Includes --info.\n"
|
||||
" --verbose Increase verbosity of debug output. Includes --debug.\n"
|
||||
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
@@ -181,9 +152,6 @@ def main(sysArgs=None):
|
||||
elif inOpt == "--debug":
|
||||
logLevel = logging.DEBUG
|
||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--verbose":
|
||||
logLevel = VERBOSE
|
||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--style":
|
||||
qtStyle = inArg
|
||||
elif inOpt == "--config":
|
||||
@@ -193,9 +161,6 @@ def main(sysArgs=None):
|
||||
elif inOpt == "--testmode":
|
||||
testMode = True
|
||||
|
||||
# Set Config Options
|
||||
CONFIG.cmdOpen = cmdOpen
|
||||
|
||||
# Set Logging
|
||||
cHandle = logging.StreamHandler()
|
||||
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
|
||||
@@ -214,14 +179,14 @@ def main(sysArgs=None):
|
||||
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
|
||||
)
|
||||
errorCode |= 0x04
|
||||
if CONFIG.verQtValue < 50300:
|
||||
if CONFIG.verQtValue < 51000:
|
||||
errorData.append(
|
||||
"At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
|
||||
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString
|
||||
)
|
||||
errorCode |= 0x08
|
||||
if CONFIG.verPyQtValue < 50300:
|
||||
if CONFIG.verPyQtValue < 51000:
|
||||
errorData.append(
|
||||
"At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
|
||||
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString
|
||||
)
|
||||
errorCode |= 0x10
|
||||
|
||||
@@ -280,7 +245,6 @@ def main(sysArgs=None):
|
||||
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
||||
nwApp.setApplicationName(CONFIG.appName)
|
||||
nwApp.setApplicationVersion(__version__)
|
||||
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
|
||||
nwApp.setOrganizationDomain(__domain__)
|
||||
|
||||
# Connect the exception handler before making the main GUI
|
||||
@@ -289,9 +253,7 @@ def main(sysArgs=None):
|
||||
# Launch main GUI
|
||||
CONFIG.initLocalisation(nwApp)
|
||||
nwGUI = GuiMain()
|
||||
if not nwGUI.hasProject:
|
||||
nwGUI.showProjectLoadDialog()
|
||||
nwGUI.releaseNotes()
|
||||
nwGUI.postLaunchTasks(cmdOpen)
|
||||
|
||||
sys.exit(nwApp.exec_())
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
[Map]
|
||||
add = typ_plus.svg
|
||||
backward = typ_chevron-left.svg
|
||||
bookmark = typ_bookmark.svg
|
||||
bullet-off = typ_media-record-outline.svg
|
||||
bullet-on = typ_media-record.svg
|
||||
check = typ_tick.svg
|
||||
clear = typ_backspace.svg
|
||||
checked = mixed_input-checked.svg
|
||||
close = typ_times.svg
|
||||
cls_archive = typ_delete.svg
|
||||
cls_character = typ_user.svg
|
||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
||||
cls_trash = typ_trash.svg
|
||||
cls_world = typ_location.svg
|
||||
cross = typ_times.svg
|
||||
delete = typ_delete.svg
|
||||
doc_h0 = mixed_heading0.svg
|
||||
doc_h1 = mixed_heading1.svg
|
||||
doc_h2 = mixed_heading2.svg
|
||||
doc_h3 = mixed_heading3.svg
|
||||
doc_h4 = mixed_heading4.svg
|
||||
done = typ_input-checked.svg
|
||||
down = typ_chevron-down.svg
|
||||
edit = typ_pencil.svg
|
||||
forward = typ_chevron-right.svg
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
noncheckable = mixed_input-none.svg
|
||||
proj_chapter = mixed_document-chapter.svg
|
||||
proj_details = typ_th-list-grey.svg
|
||||
proj_document = typ_document-text.svg
|
||||
proj_folder = typ_folder.svg
|
||||
proj_note = mixed_document-note.svg
|
||||
proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
reference = typ_at.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
save = typ_download.svg
|
||||
search = typ_search.svg
|
||||
search_cancel = typ_cancel-grey.svg
|
||||
search_case = nw_search-case.svg
|
||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
||||
status_time = typ_stopwatch-grey.svg
|
||||
sticky-off = typ_pin-outline.svg
|
||||
sticky-on = typ_pin.svg
|
||||
unchecked = mixed_input-unchecked.svg
|
||||
up = typ_chevron-up.svg
|
||||
view_build = typ_export.svg
|
||||
view_editor = mixed_edit.svg
|
||||
view_novel = typ_book-grey.svg
|
||||
view_outline = typ_puzzle-outline.svg
|
||||
|
||||
deco_doc_h0 = nw_deco-h0.svg
|
||||
deco_doc_h1 = nw_deco-h1.svg
|
||||
deco_doc_h2 = nw_deco-h2.svg
|
||||
deco_doc_h3 = nw_deco-h3.svg
|
||||
deco_doc_h4 = nw_deco-h4.svg
|
||||
deco_doc_more = nw_deco-noveltree-more.svg
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2161">
|
||||
<metadata
|
||||
id="metadata2167">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2165" />
|
||||
<path
|
||||
id="rect12986"
|
||||
style="display:inline;opacity:0.95;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||
<path
|
||||
id="rect935"
|
||||
style="fill:#999999;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
|
||||
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||
<path
|
||||
id="path2157"
|
||||
style="display:inline;fill:#595959;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
|
||||
<path
|
||||
id="path902"
|
||||
style="display:inline;fill:#848484;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
|
||||
<path
|
||||
id="path2157-0"
|
||||
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2469"
|
||||
cx="19.470242"
|
||||
cy="9.4077778"
|
||||
r="2.2222221" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2469"
|
||||
cx="19.470242"
|
||||
cy="9.4077778"
|
||||
r="2.2222221" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="14.592222"
|
||||
cx="19.470242"
|
||||
id="circle2471"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<g
|
||||
transform="matrix(1.1111111,0,0,1.1111111,-1.3333334,-1.3333332)"
|
||||
id="g2616">
|
||||
<path
|
||||
d="m 4.029904,3.7531206 c -1.0041608,1.0041608 -1.0041608,2.6319041 0,3.6360649 L 8.6392724,11.999839 4.029904,16.610493 c -1.0041608,1.004161 -1.0041608,2.631904 0,3.636065 C 4.5313415,20.749281 5.189639,21 5.8479364,21 c 0.658298,0 1.316595,-0.250719 1.818033,-0.753442 L 15.913973,11.999839 7.6659694,3.7531206 c -1.002876,-1.0041608 -2.6331904,-1.0041608 -3.6360654,0 z"
|
||||
id="path1895"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28573728" />
|
||||
<circle
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path2467"
|
||||
cx="18.723217"
|
||||
cy="5"
|
||||
r="2" />
|
||||
<circle
|
||||
r="2"
|
||||
cy="9.6669998"
|
||||
cx="18.723217"
|
||||
id="circle2469"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2471"
|
||||
cx="18.723217"
|
||||
cy="14.333"
|
||||
r="2" />
|
||||
<circle
|
||||
r="2"
|
||||
cy="19"
|
||||
cx="18.723217"
|
||||
id="circle2473"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,39 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2287">
|
||||
<defs
|
||||
id="defs2291" />
|
||||
<metadata
|
||||
id="metadata2281">
|
||||
<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>
|
||||
<path
|
||||
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#848484"
|
||||
stroke-width="1.4286"
|
||||
id="path2283" />
|
||||
<path
|
||||
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
|
||||
fill="#9c9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="path2285" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,44 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2931">
|
||||
<defs
|
||||
id="defs2935" />
|
||||
<metadata
|
||||
id="metadata2925">
|
||||
<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>
|
||||
<path
|
||||
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#848484"
|
||||
stroke-width="1.4286"
|
||||
id="path2927" />
|
||||
<rect
|
||||
x="7"
|
||||
y="10.1"
|
||||
width="10"
|
||||
height="3.8"
|
||||
rx="1.9"
|
||||
ry="1.9"
|
||||
fill="#f99157"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="rect2929" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg3574">
|
||||
<defs
|
||||
id="defs3578" />
|
||||
<metadata
|
||||
id="metadata3568">
|
||||
<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>
|
||||
<path
|
||||
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#848484"
|
||||
stroke-width="1.4286"
|
||||
id="path3570" />
|
||||
<path
|
||||
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
|
||||
fill="#d64848"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="path3572" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2161"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata2167">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2165" />
|
||||
<path
|
||||
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
|
||||
id="path3096"
|
||||
style="fill:#848484;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 928 B |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg13156">
|
||||
<metadata
|
||||
id="metadata13162">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13160" />
|
||||
<path
|
||||
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z"
|
||||
id="path13154"
|
||||
style="stroke-width:1.00151;fill:#d64848;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -9,9 +9,9 @@
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2608">
|
||||
id="svg901">
|
||||
<metadata
|
||||
id="metadata2614">
|
||||
id="metadata907">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@@ -23,9 +23,9 @@
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2612" />
|
||||
id="defs905" />
|
||||
<path
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||
id="path2606"
|
||||
style="stroke-width:1.05081;fill:#6699cc;fill-opacity:1" />
|
||||
id="path899"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.05081" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg13801">
|
||||
<metadata
|
||||
id="metadata13807">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13805" />
|
||||
<path
|
||||
style="display:inline;fill:#cda66f;fill-opacity:1"
|
||||
id="path6288"
|
||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
||||
<path
|
||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
||||
id="path13799"
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg3369">
|
||||
<metadata
|
||||
id="metadata3375">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3373" />
|
||||
<path
|
||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z"
|
||||
id="path3367"
|
||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5204">
|
||||
<metadata
|
||||
id="metadata5210">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5208" />
|
||||
<path
|
||||
id="rect928"
|
||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg11367"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata11373">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs11371" />
|
||||
<path
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.28570998"
|
||||
id="path11365"
|
||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z" />
|
||||
<path
|
||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z"
|
||||
id="path16196"
|
||||
style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.28570998" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg873">
|
||||
<metadata
|
||||
id="metadata879">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs877" />
|
||||
<path
|
||||
id="path871"
|
||||
style="display:inline;fill:#6699cc;fill-opacity:1;stroke-width:1.19007"
|
||||
d="m 10.330622,1.9999997 c -4.5924663,0 -8.3306193,3.738153 -8.3306193,8.3306193 0,1.391929 0.3467825,2.702587 0.9532665,3.857242 0.084308,-0.108411 0.1590865,-0.228838 0.2534294,-0.323181 L 4.7482003,12.323178 C 4.5237338,11.697056 4.378519,11.032991 4.3785189,10.330619 c 0,-3.2810129 2.6710902,-5.949778 5.9521031,-5.949778 0.702303,0 1.36649,0.1429324 1.992559,0.3673563 L 14.141363,2.9300158 C 12.997884,2.3384803 11.704084,1.9999997 10.330622,1.9999997 Z m 5.635898,3.4852355 -0.60451,0.6045104 2.545919,2.5482442 0.460357,-0.4626829 C 18.332278,8.0413114 18.299136,7.9066594 18.256684,7.7754 Z m 0.292955,4.4245521 c 0.0083,0.1189097 0.01733,0.2373427 0.01861,0.3580557 l 0.169728,-0.169728 z m 2.059986,2.6993717 -5.710299,5.710299 c 0.105352,-0.03032 0.217183,-0.04484 0.320855,-0.07906 l 0.892816,0.89049 1.799581,1.797261 0.07208,0.07208 0.07672,0.06743 C 16.486443,21.66984 17.375591,22 18.275281,22 c 2.054056,0 3.724716,-1.671795 3.724716,-3.72704 0,-0.999657 -0.390465,-1.936781 -1.099745,-2.638921 l -1.129969,-1.174145 -0.639387,-0.639386 -0.890489,-0.892815 c 0.03403,-0.103138 0.04885,-0.213757 0.07906,-0.318531 z m -11.7600542,2.28784 -0.4929085,0.492908 0.5417343,0.541734 0.57196,-0.571959 C 6.960254,15.221393 6.7588057,15.062002 6.5594068,14.896999 Z m 2.7179722,1.285747 -1.209021,1.209021 0.5417345,0.541734 1.6577535,-1.655429 c -0.3372847,-0.0036 -0.668706,-0.03764 -0.990467,-0.09533 z" />
|
||||
<path
|
||||
d="m 15.96652,2.4766329 c -0.396657,0 -0.792437,0.1507259 -1.095095,0.4533829 L 3.57173,14.229712 c -0.3026571,0.302657 -0.5759103,0.751428 -0.7742385,1.227621 -0.1983282,0.47826 -0.3208556,0.99063 -0.3208556,1.418274 v 4.647756 h 4.6477558 c 0.427645,0 0.9376894,-0.122526 1.4159495,-0.320855 0.4782601,-0.198328 0.9272894,-0.471583 1.2299463,-0.774239 L 21.069984,9.1285734 c 0.302656,-0.302657 0.453382,-0.7017958 0.453382,-1.0974191 0,-0.3966562 -0.150726,-0.7924369 -0.453382,-1.0950939 L 17.063939,2.9300158 C 16.761281,2.6273588 16.362143,2.4766329 15.96652,2.4766329 Z m 0,2.2785396 3.275981,3.2759818 -1.334572,1.3368983 -3.275982,-3.2759819 z M 13.901884,6.8221334 15.173682,8.0939305 6.6082326,16.661704 5.3364357,15.387582 Z m 2.004185,2.0041848 1.271796,1.2717968 -8.5654475,8.565448 -1.2741221,-1.271796 z M 4.7063497,16.250176 c 0.010333,-0.02065 3.0644031,3.034179 3.0644031,3.034179 C 7.43814,19.422771 7.2039295,19.456408 7.1243917,19.456408 H 5.5759147 L 4.5435968,18.424084 v -1.548477 c 0,-0.07953 0.033633,-0.315548 0.1627529,-0.625436 z"
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.19007"
|
||||
id="path15590" />
|
||||
<path
|
||||
id="rect19553"
|
||||
d="m 2.4766359,18.937425 v 2.585938 h 2.5605469 z"
|
||||
style="display:inline;opacity:0.95;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.88249397;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg7030"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata7036">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7034" />
|
||||
<g
|
||||
style="fill:#aeaeae;fill-opacity:1"
|
||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
||||
id="g7028">
|
||||
<path
|
||||
style="fill:#aeaeae;fill-opacity:1"
|
||||
id="path7026"
|
||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg896">
|
||||
<metadata
|
||||
id="metadata902">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs900" />
|
||||
<path
|
||||
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
|
||||
id="path894"
|
||||
style="stroke-width:1.3846;fill:#99cc99;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
[Map]
|
||||
add = typ_plus.svg
|
||||
backward = typ_chevron-left.svg
|
||||
bookmark = typ_bookmark.svg
|
||||
bullet-off = typ_media-record-outline.svg
|
||||
bullet-on = typ_media-record.svg
|
||||
check = typ_tick.svg
|
||||
clear = typ_backspace.svg
|
||||
checked = mixed_input-checked.svg
|
||||
close = typ_times.svg
|
||||
cls_archive = typ_delete.svg
|
||||
cls_character = typ_user.svg
|
||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
||||
cls_trash = typ_trash.svg
|
||||
cls_world = typ_location.svg
|
||||
cross = typ_times.svg
|
||||
delete = typ_delete.svg
|
||||
doc_h0 = mixed_heading0.svg
|
||||
doc_h1 = mixed_heading1.svg
|
||||
doc_h2 = mixed_heading2.svg
|
||||
doc_h3 = mixed_heading3.svg
|
||||
doc_h4 = mixed_heading4.svg
|
||||
done = typ_input-checked.svg
|
||||
down = typ_chevron-down.svg
|
||||
edit = typ_pencil.svg
|
||||
forward = typ_chevron-right.svg
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
noncheckable = mixed_input-none.svg
|
||||
proj_chapter = mixed_document-chapter.svg
|
||||
proj_details = typ_th-list-grey.svg
|
||||
proj_document = typ_document-text.svg
|
||||
proj_folder = typ_folder.svg
|
||||
proj_note = mixed_document-note.svg
|
||||
proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
reference = typ_at.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
save = typ_download.svg
|
||||
search = typ_search.svg
|
||||
search_cancel = typ_cancel-grey.svg
|
||||
search_case = nw_search-case.svg
|
||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
||||
status_time = typ_stopwatch-grey.svg
|
||||
sticky-off = typ_pin-outline.svg
|
||||
sticky-on = typ_pin.svg
|
||||
unchecked = mixed_input-unchecked.svg
|
||||
up = typ_chevron-up.svg
|
||||
view_build = typ_export.svg
|
||||
view_editor = mixed_edit.svg
|
||||
view_novel = typ_book-grey.svg
|
||||
view_outline = typ_puzzle-outline.svg
|
||||
|
||||
deco_doc_h0 = nw_deco-h0.svg
|
||||
deco_doc_h1 = nw_deco-h1.svg
|
||||
deco_doc_h2 = nw_deco-h2.svg
|
||||
deco_doc_h3 = nw_deco-h3.svg
|
||||
deco_doc_h4 = nw_deco-h4.svg
|
||||
deco_doc_more = nw_deco-noveltree-more.svg
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2161">
|
||||
<metadata
|
||||
id="metadata2167">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2165" />
|
||||
<path
|
||||
id="rect12986"
|
||||
style="display:inline;opacity:0.95;fill:#ededed;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||
<path
|
||||
id="rect935"
|
||||
style="fill:#787878;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
|
||||
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||
<path
|
||||
id="path2157"
|
||||
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
|
||||
<path
|
||||
id="path902"
|
||||
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
|
||||
<path
|
||||
id="path2157-0"
|
||||
style="display:inline;fill:#ededed;fill-opacity:1;stroke-width:1.22222"
|
||||
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2469"
|
||||
cx="19.470242"
|
||||
cy="9.4077778"
|
||||
r="2.2222221" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2469"
|
||||
cx="19.470242"
|
||||
cy="9.4077778"
|
||||
r="2.2222221" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="14.592222"
|
||||
cx="19.470242"
|
||||
id="circle2471"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="4.2222223"
|
||||
cx="19.470242"
|
||||
id="path2467"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2469"
|
||||
cx="19.470242"
|
||||
cy="9.4077778"
|
||||
r="2.2222221" />
|
||||
<circle
|
||||
r="2.2222221"
|
||||
cy="14.592222"
|
||||
cx="19.470242"
|
||||
id="circle2471"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle2473"
|
||||
cx="19.470242"
|
||||
cy="19.777779"
|
||||
r="2.2222221" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,39 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg879">
|
||||
<defs
|
||||
id="defs883" />
|
||||
<metadata
|
||||
id="metadata873">
|
||||
<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>
|
||||
<path
|
||||
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#333"
|
||||
stroke-width="1.4286"
|
||||
id="path875" />
|
||||
<path
|
||||
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
|
||||
fill="#718c00"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="path877" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,44 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg973">
|
||||
<defs
|
||||
id="defs977" />
|
||||
<metadata
|
||||
id="metadata967">
|
||||
<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>
|
||||
<path
|
||||
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#333"
|
||||
stroke-width="1.4286"
|
||||
id="path969" />
|
||||
<rect
|
||||
x="7"
|
||||
y="10.1"
|
||||
width="10"
|
||||
height="3.8"
|
||||
rx="1.9"
|
||||
ry="1.9"
|
||||
fill="#f5871f"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="rect971" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
||||
<?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"
|
||||
width="24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg1616">
|
||||
<defs
|
||||
id="defs1620" />
|
||||
<metadata
|
||||
id="metadata1610">
|
||||
<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>
|
||||
<path
|
||||
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||
fill="#333"
|
||||
stroke-width="1.4286"
|
||||
id="path1612" />
|
||||
<path
|
||||
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
|
||||
fill="#c82829"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.111"
|
||||
id="path1614" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2161"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata2167">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2165" />
|
||||
<path
|
||||
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
|
||||
id="path3096"
|
||||
style="fill:#848484;fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 928 B |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg13156"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata13162">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13160" />
|
||||
<path
|
||||
style="stroke-width:1.00151;fill:#c82829;fill-opacity:1"
|
||||
id="path13154"
|
||||
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -5,13 +5,13 @@
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg2608"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
width="24"
|
||||
version="1.2">
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg901">
|
||||
<metadata
|
||||
id="metadata2614">
|
||||
id="metadata907">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@@ -23,9 +23,9 @@
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2612" />
|
||||
id="defs905" />
|
||||
<path
|
||||
style="stroke-width:1.05081;fill:#4271ae;fill-opacity:1"
|
||||
id="path2606"
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z" />
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||
id="path899"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.05081" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg13801">
|
||||
<metadata
|
||||
id="metadata13807">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13805" />
|
||||
<path
|
||||
style="display:inline;fill:#a2694f;fill-opacity:1"
|
||||
id="path6288"
|
||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
||||
<path
|
||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
||||
id="path13799"
|
||||
style="display:inline;fill:#718c00;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg3369"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata3375">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3373" />
|
||||
<path
|
||||
style="fill:#333333;fill-opacity:1;stroke-width:1"
|
||||
id="path3367"
|
||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5204">
|
||||
<metadata
|
||||
id="metadata5210">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5208" />
|
||||
<path
|
||||
id="rect928"
|
||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg11367">
|
||||
<metadata
|
||||
id="metadata11373">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs11371" />
|
||||
<path
|
||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z"
|
||||
id="path11365"
|
||||
style="display:inline;fill:#718c00;fill-opacity:1;stroke-width:1.28570998" />
|
||||
<path
|
||||
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.28570998"
|
||||
id="path16196"
|
||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg7030">
|
||||
<metadata
|
||||
id="metadata7036">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7034" />
|
||||
<g
|
||||
id="g7028"
|
||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
||||
style="fill:#333333;fill-opacity:1">
|
||||
<path
|
||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z"
|
||||
id="path7026"
|
||||
style="fill:#333333;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg896">
|
||||
<metadata
|
||||
id="metadata902">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs900" />
|
||||
<path
|
||||
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
|
||||
id="path894"
|
||||
style="stroke-width:1.3846;fill:#718c00;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2,11 +2,11 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.7 Beta 1</h2>
|
||||
<p><i>Released on 17 May 2022</i></p>
|
||||
<h2>Release Notes for 2.0 RC 2</h2>
|
||||
<p><i>Released on 13 November 2022</i></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
|
||||
<p>This is a release candidate 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>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
[Main]
|
||||
name = Default System Theme
|
||||
name = Default Theme
|
||||
description = Qt standard colours
|
||||
icontheme = typicons_light
|
||||
@@ -1,16 +1,18 @@
|
||||
[Main]
|
||||
name = Default Dark Theme
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
name = Default Dark Theme
|
||||
description = The novelWriter standard dark theme
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
icontheme = typicons_dark
|
||||
|
||||
[Palette]
|
||||
window = 54, 54, 54
|
||||
windowtext = 174, 174, 174
|
||||
base = 62, 62, 62
|
||||
alternatebase = 67, 67, 67
|
||||
alternatebase = 78, 78, 78
|
||||
text = 174, 174, 174
|
||||
tooltipbase = 255, 255, 192
|
||||
tooltiptext = 21, 21, 13
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* Default Theme: Dark
|
||||
* This theme doesn't use any custom styles, so the file is only here as
|
||||
* an example. There doesn't have to be a styles.qss file in the folder.
|
||||
*/
|
||||
@@ -5,12 +5,13 @@ credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
icontheme = typicons_dark
|
||||
|
||||
[Palette]
|
||||
window = 0, 43, 54
|
||||
windowtext = 253, 246, 227
|
||||
base = 7, 54, 66
|
||||
alternatebase = 67, 67, 67
|
||||
alternatebase = 0, 43, 54
|
||||
text = 253, 246, 227
|
||||
tooltipbase = 133, 153, 0
|
||||
tooltiptext = 0, 43, 54
|
||||
|
||||
@@ -5,6 +5,7 @@ credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
icontheme = typicons_light
|
||||
|
||||
[Palette]
|
||||
window = 238, 232, 213
|
||||
|
||||
@@ -23,11 +23,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from configparser import ConfigParser
|
||||
|
||||
@@ -45,52 +46,55 @@ logger = logging.getLogger(__name__)
|
||||
# Checker Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def checkString(value, default, allowNone=False):
|
||||
def checkStringNone(value, default):
|
||||
"""Check if a variable is a string or a None.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
if value is None or value == "None":
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def checkInt(value, default, allowNone=False):
|
||||
"""Check if a variable is an integer or a None.
|
||||
def checkString(value, default):
|
||||
"""Check if a variable is a string.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def checkInt(value, default):
|
||||
"""Check if a variable is an integer.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkFloat(value, default, allowNone=False):
|
||||
"""Check if a variable is a float or a None.
|
||||
def checkFloat(value, default):
|
||||
"""Check if a variable is a float.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkBool(value, default, allowNone=False):
|
||||
"""Check if a variable is a boolean or a None.
|
||||
def checkBool(value, default):
|
||||
"""Check if a variable is a boolean.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
|
||||
if isinstance(value, str):
|
||||
if value == "True":
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
check = value.lower()
|
||||
if check in ("true", "yes", "on"):
|
||||
return True
|
||||
elif value == "False":
|
||||
elif check in ("false", "no", "off"):
|
||||
return False
|
||||
else:
|
||||
return default
|
||||
|
||||
elif isinstance(value, int):
|
||||
if value == 1:
|
||||
return True
|
||||
@@ -98,7 +102,6 @@ def checkBool(value, default, allowNone=False):
|
||||
return False
|
||||
else:
|
||||
return default
|
||||
|
||||
return default
|
||||
|
||||
|
||||
@@ -112,6 +115,26 @@ def checkHandle(value, default, allowNone=False):
|
||||
return default
|
||||
|
||||
|
||||
def checkUuid(value, default):
|
||||
"""Try to process a value as an uuid, or return a default.
|
||||
"""
|
||||
try:
|
||||
return str(uuid.UUID(value))
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkPath(value, default):
|
||||
"""Check if a value is a valid path. Non-empty strings are accepted.
|
||||
"""
|
||||
if isinstance(value, Path):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
if value.strip():
|
||||
return Path(value)
|
||||
return default
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Validator Functions
|
||||
# =============================================================================================== #
|
||||
@@ -174,16 +197,6 @@ def hexToInt(value, default=0):
|
||||
return default
|
||||
|
||||
|
||||
def checkIntRange(value, first, last, default):
|
||||
"""Check that an int is in a given range. If it isn't, return the
|
||||
default value.
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
if value >= first and value <= last:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def minmax(value, minVal, maxVal):
|
||||
"""Make sure an integer is between min and max value (inclusive).
|
||||
"""
|
||||
@@ -225,25 +238,25 @@ def formatInt(value):
|
||||
return str(value)
|
||||
|
||||
|
||||
def formatTimeStamp(theTime, fileSafe=False):
|
||||
def formatTimeStamp(value, fileSafe=False):
|
||||
"""Take a number (on the format returned by time.time()) and convert
|
||||
it to a timestamp string.
|
||||
"""
|
||||
if fileSafe:
|
||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP)
|
||||
return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
|
||||
else:
|
||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
|
||||
return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP)
|
||||
|
||||
|
||||
def formatTime(tS):
|
||||
def formatTime(t):
|
||||
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
|
||||
if a full day or longer.
|
||||
"""
|
||||
if isinstance(tS, int):
|
||||
if tS >= 86400:
|
||||
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
if isinstance(t, int):
|
||||
if t >= 86400:
|
||||
return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||
else:
|
||||
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||
return "ERROR"
|
||||
|
||||
|
||||
@@ -258,12 +271,18 @@ def simplified(string):
|
||||
return " ".join(str(string).strip().split())
|
||||
|
||||
|
||||
def yesNo(value):
|
||||
"""Convert a boolean evaluated variable to a yes or no.
|
||||
"""
|
||||
return "yes" if value else "no"
|
||||
|
||||
|
||||
def splitVersionNumber(value):
|
||||
"""Split a version string on the form aa.bb.cc into major, minor
|
||||
and patch, and computes an integer value aabbcc.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return [0, 0, 0, 0]
|
||||
return 0, 0, 0, 0
|
||||
|
||||
vMajor = 0
|
||||
vMinor = 0
|
||||
@@ -282,114 +301,114 @@ def splitVersionNumber(value):
|
||||
|
||||
vInt = vMajor*10000 + vMinor*100 + vPatch
|
||||
|
||||
return [vMajor, vMinor, vPatch, vInt]
|
||||
return vMajor, vMinor, vPatch, vInt
|
||||
|
||||
|
||||
def transferCase(theSource, theTarget):
|
||||
def transferCase(source, target):
|
||||
"""Transfers the case of the source word to the target word. This
|
||||
will consider all upper or lower, and first char capitalisation.
|
||||
"""
|
||||
theResult = theTarget
|
||||
theResult = target
|
||||
|
||||
if not isinstance(theSource, str) or not isinstance(theTarget, str):
|
||||
if not isinstance(source, str) or not isinstance(target, str):
|
||||
return theResult
|
||||
if len(theTarget) < 1 or len(theSource) < 1:
|
||||
if len(target) < 1 or len(source) < 1:
|
||||
return theResult
|
||||
|
||||
if theSource.istitle():
|
||||
theResult = theTarget.title()
|
||||
if source.istitle():
|
||||
theResult = target.title()
|
||||
|
||||
if theSource.isupper():
|
||||
theResult = theTarget.upper()
|
||||
elif theSource.islower():
|
||||
theResult = theTarget.lower()
|
||||
if source.isupper():
|
||||
theResult = target.upper()
|
||||
elif source.islower():
|
||||
theResult = target.lower()
|
||||
|
||||
return theResult
|
||||
|
||||
|
||||
def fuzzyTime(secDiff):
|
||||
def fuzzyTime(seconds):
|
||||
"""Converts a time difference in seconds into a fuzzy time string.
|
||||
"""
|
||||
if secDiff < 0:
|
||||
if seconds < 0:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "in the future"
|
||||
)
|
||||
elif secDiff < 30:
|
||||
elif seconds < 30:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "just now"
|
||||
)
|
||||
elif secDiff < 90:
|
||||
elif seconds < 90:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a minute ago"
|
||||
)
|
||||
elif secDiff < 3300: # 55 minutes
|
||||
elif seconds < 3300: # 55 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} minutes ago"
|
||||
).format(int(round(secDiff/60)))
|
||||
elif secDiff < 5400: # 90 minutes
|
||||
).format(int(round(seconds/60)))
|
||||
elif seconds < 5400: # 90 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "an hour ago"
|
||||
)
|
||||
elif secDiff < 84600: # 23.5 hours
|
||||
elif seconds < 84600: # 23.5 hours
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} hours ago"
|
||||
).format(int(round(secDiff/3600)))
|
||||
elif secDiff < 129600: # 1.5 days
|
||||
).format(int(round(seconds/3600)))
|
||||
elif seconds < 129600: # 1.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a day ago"
|
||||
)
|
||||
elif secDiff < 561600: # 6.5 days
|
||||
elif seconds < 561600: # 6.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} days ago"
|
||||
).format(int(round(secDiff/86400)))
|
||||
elif secDiff < 907200: # 10.5 days
|
||||
).format(int(round(seconds/86400)))
|
||||
elif seconds < 907200: # 10.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a week ago"
|
||||
)
|
||||
elif secDiff < 2419200: # 28 days
|
||||
elif seconds < 2419200: # 28 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} weeks ago"
|
||||
).format(int(round(secDiff/604800)))
|
||||
elif secDiff < 3888000: # 45 days
|
||||
).format(int(round(seconds/604800)))
|
||||
elif seconds < 3888000: # 45 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a month ago"
|
||||
)
|
||||
elif secDiff < 29808000: # 345 days
|
||||
elif seconds < 29808000: # 345 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} months ago"
|
||||
).format(int(round(secDiff/2592000)))
|
||||
elif secDiff < 47336400: # 1.5 years
|
||||
).format(int(round(seconds/2592000)))
|
||||
elif seconds < 47336400: # 1.5 years
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a year ago"
|
||||
)
|
||||
else:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} years ago"
|
||||
).format(int(round(secDiff/31557600)))
|
||||
).format(int(round(seconds/31557600)))
|
||||
|
||||
|
||||
def numberToRoman(numVal, toLower=False):
|
||||
def numberToRoman(value, toLower=False):
|
||||
"""Convert an integer to a Roman number.
|
||||
"""
|
||||
if not isinstance(numVal, int):
|
||||
if not isinstance(value, int):
|
||||
return "NAN"
|
||||
if numVal < 1 or numVal > 4999:
|
||||
if value < 1 or value > 4999:
|
||||
return "OOR"
|
||||
|
||||
theValues = [
|
||||
lookup = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
||||
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
romNum = ""
|
||||
for theDiv, theSym in theValues:
|
||||
n = numVal//theDiv
|
||||
romNum += n*theSym
|
||||
numVal -= n*theDiv
|
||||
if numVal <= 0:
|
||||
roman = ""
|
||||
for divisor, symbol in lookup:
|
||||
n = value//divisor
|
||||
roman += n*symbol
|
||||
value -= n*divisor
|
||||
if value <= 0:
|
||||
break
|
||||
|
||||
return romNum.lower() if toLower else romNum
|
||||
return roman.lower() if toLower else roman
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
@@ -447,51 +466,47 @@ def jsonEncode(data, n=0, nmax=0):
|
||||
# File and File System Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def readTextFile(filePath):
|
||||
def readTextFile(path):
|
||||
"""Read the content of a text file in a robust manner.
|
||||
"""
|
||||
if not os.path.isfile(filePath):
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
return ""
|
||||
|
||||
fileText = ""
|
||||
try:
|
||||
with open(filePath, mode="r", encoding="utf-8") as inFile:
|
||||
fileText = inFile.read()
|
||||
return path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
logger.error("Could not read file: %s", filePath)
|
||||
logger.error("Could not read file: %s", path)
|
||||
logException()
|
||||
return ""
|
||||
|
||||
return fileText
|
||||
|
||||
|
||||
def makeFileNameSafe(value):
|
||||
"""Returns a filename safe string of the value.
|
||||
"""
|
||||
cleanName = ""
|
||||
clean = ""
|
||||
for c in str(value).strip():
|
||||
if c.isalpha() or c.isdigit() or c == " ":
|
||||
cleanName += c
|
||||
return cleanName
|
||||
clean += c
|
||||
return clean
|
||||
|
||||
|
||||
def sha256sum(filePath):
|
||||
def sha256sum(path):
|
||||
"""Make a shasum of a file using a buffer.
|
||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
||||
"""
|
||||
hDigest = hashlib.sha256()
|
||||
digest = hashlib.sha256()
|
||||
bData = bytearray(65536)
|
||||
mData = memoryview(bData)
|
||||
try:
|
||||
with open(filePath, mode="rb", buffering=0) as inFile:
|
||||
with open(path, mode="rb", buffering=0) as inFile:
|
||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
||||
hDigest.update(mData[:n])
|
||||
digest.update(mData[:n])
|
||||
except Exception:
|
||||
logger.error("Could not create sha256sum of: %s", filePath)
|
||||
logger.error("Could not create sha256sum of: %s", path)
|
||||
logException()
|
||||
return None
|
||||
|
||||
return hDigest.hexdigest()
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
@@ -513,87 +528,64 @@ def getGuiItem(objName):
|
||||
|
||||
class NWConfigParser(ConfigParser):
|
||||
|
||||
CNF_STR = 0
|
||||
CNF_INT = 1
|
||||
CNF_FLOAT = 2
|
||||
CNF_BOOL = 3
|
||||
CNF_S_LST = 4
|
||||
CNF_I_LST = 5
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def rdStr(self, section, option, default):
|
||||
"""Read string value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_STR)
|
||||
return self.get(section, option, fallback=default)
|
||||
|
||||
def rdInt(self, section, option, default):
|
||||
"""Read integer value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_INT)
|
||||
try:
|
||||
return self.getint(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdFlt(self, section, option, default):
|
||||
"""Read float value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_FLOAT)
|
||||
try:
|
||||
return self.getfloat(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdBool(self, section, option, default):
|
||||
"""Read boolean value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_BOOL)
|
||||
try:
|
||||
return self.getboolean(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdPath(self, section, option, default):
|
||||
"""Read a path value.
|
||||
"""
|
||||
return checkPath(self.get(section, option, fallback=default), default)
|
||||
|
||||
def rdStrList(self, section, option, default):
|
||||
"""Read string list.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_S_LST)
|
||||
result = default.copy() if isinstance(default, list) else []
|
||||
if self.has_option(section, option):
|
||||
data = self.get(section, option, fallback="").split(",")
|
||||
for i in range(min(len(data), len(result))):
|
||||
result[i] = data[i].strip()
|
||||
return result
|
||||
|
||||
def rdIntList(self, section, option, default):
|
||||
"""Read integer list.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_I_LST)
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _unpackList(self, value, default, type):
|
||||
"""Unpack a comma-separated string of items into a list.
|
||||
"""
|
||||
inList = value.split(",")
|
||||
outList = []
|
||||
if isinstance(default, list):
|
||||
outList = default.copy()
|
||||
for i in range(min(len(inList), len(outList))):
|
||||
try:
|
||||
if type == self.CNF_S_LST:
|
||||
outList[i] = inList[i].strip()
|
||||
elif type == self.CNF_I_LST:
|
||||
outList[i] = int(inList[i].strip())
|
||||
except Exception:
|
||||
continue
|
||||
return outList
|
||||
|
||||
def _parseLine(self, section, option, default, type):
|
||||
"""Parse a line and return the correct datatype.
|
||||
"""
|
||||
result = default.copy() if isinstance(default, list) else []
|
||||
if self.has_option(section, option):
|
||||
try:
|
||||
if type == self.CNF_STR:
|
||||
return self.get(section, option)
|
||||
elif type == self.CNF_INT:
|
||||
return self.getint(section, option)
|
||||
elif type == self.CNF_FLOAT:
|
||||
return self.getfloat(section, option)
|
||||
elif type == self.CNF_BOOL:
|
||||
return self.getboolean(section, option)
|
||||
elif type in (self.CNF_I_LST, self.CNF_S_LST):
|
||||
return self._unpackList(self.get(section, option), default, type)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", str(section), str(option))
|
||||
logException()
|
||||
return default
|
||||
|
||||
return default
|
||||
data = self.get(section, option, fallback="").split(",")
|
||||
for i in range(min(len(data), len(result))):
|
||||
result[i] = checkInt(data[i].strip(), result[i])
|
||||
return result
|
||||
|
||||
# END Class NWConfigParser
|
||||
|
||||
@@ -68,6 +68,7 @@ class nwHeaders:
|
||||
|
||||
class nwFiles:
|
||||
|
||||
CONF_FILE = "novelwriter.conf"
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
@@ -157,6 +158,7 @@ class nwLabels:
|
||||
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
|
||||
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
|
||||
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
|
||||
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
|
||||
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
|
||||
}
|
||||
KEY_NAME = {
|
||||
|
||||
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from novelwriter.core.document import NWDoc
|
||||
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
|
||||
from novelwriter.core.index import countWords
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||
@@ -28,8 +28,10 @@ from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.tomd import ToMarkdown
|
||||
|
||||
__all__ = [
|
||||
"DocMerger",
|
||||
"DocSplitter",
|
||||
"ProjectBuilder",
|
||||
"countWords",
|
||||
"NWDoc",
|
||||
"NWProject",
|
||||
"NWSpellEnchant",
|
||||
"ToHtml",
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
"""
|
||||
novelWriter – Project Document Tools
|
||||
====================================
|
||||
A collection of tools to create and manipulate documents
|
||||
|
||||
File History:
|
||||
Created: 2022-10-02 [2.0b1] DocMerger
|
||||
Created: 2022-10-11 [2.0b1] DocSplitter
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from time import time
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.common import minmax, simplified
|
||||
from novelwriter.constants import nwItemClass
|
||||
from novelwriter.core.project import NWProject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DocMerger:
|
||||
"""Document tool for merging a set of documents into a single new
|
||||
document. The parameters are defined by the user using the
|
||||
GuiDocMerge dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.theProject = theProject
|
||||
|
||||
self._error = ""
|
||||
self._targetDoc = None
|
||||
self._targetText = []
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def getError(self):
|
||||
"""Return any collected errors.
|
||||
"""
|
||||
return self._error
|
||||
|
||||
def setTargetDoc(self, tHandle):
|
||||
"""Set the target document for the merging. Calling this
|
||||
function resets the class.
|
||||
"""
|
||||
self._targetDoc = tHandle
|
||||
self._targetText = []
|
||||
return
|
||||
|
||||
def newTargetDoc(self, srcHandle, docLabel):
|
||||
"""Create a barnd new target document based on a source handle
|
||||
and a new doc label. Calling this function resets the class.
|
||||
"""
|
||||
srcItem = self.theProject.tree[srcHandle]
|
||||
if srcItem is None:
|
||||
return None
|
||||
|
||||
newHandle = self.theProject.newFile(docLabel, srcItem.itemParent)
|
||||
newItem = self.theProject.tree[newHandle]
|
||||
newItem.setLayout(srcItem.itemLayout)
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
newItem.setImport(srcItem.itemImport)
|
||||
|
||||
self._targetDoc = newHandle
|
||||
self._targetText = []
|
||||
|
||||
return newHandle
|
||||
|
||||
def appendText(self, srcHandle, addComment, cmtPrefix):
|
||||
"""Append text from an existing document to the text buffer.
|
||||
"""
|
||||
srcItem = self.theProject.tree[srcHandle]
|
||||
if srcItem is None:
|
||||
return False
|
||||
|
||||
inDoc = self.theProject.storage.getDocument(srcHandle)
|
||||
docText = (inDoc.readDocument() or "").rstrip("\n")
|
||||
|
||||
if addComment:
|
||||
docInfo = srcItem.describeMe()
|
||||
docSt, _ = srcItem.getImportStatus(incIcon=False)
|
||||
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
|
||||
docText = cmtLine + docText
|
||||
|
||||
self._targetText.append(docText)
|
||||
|
||||
return True
|
||||
|
||||
def writeTargetDoc(self):
|
||||
"""Write the accumulated text into the designated target
|
||||
document, appending any existing text.
|
||||
"""
|
||||
if self._targetDoc is None:
|
||||
return False
|
||||
|
||||
outDoc = self.theProject.storage.getDocument(self._targetDoc)
|
||||
docText = (outDoc.readDocument() or "").rstrip("\n")
|
||||
if docText:
|
||||
self._targetText.insert(0, docText)
|
||||
|
||||
status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
|
||||
if not status:
|
||||
self._error = outDoc.getError()
|
||||
|
||||
return status
|
||||
|
||||
# END Class DocMerger
|
||||
|
||||
|
||||
class DocSplitter:
|
||||
"""Document tool for splitting a document into a set of new
|
||||
documents. The parameters are defined by the user using the
|
||||
GuiDocSplit dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, theProject, sHandle):
|
||||
|
||||
self.theProject = theProject
|
||||
|
||||
self._error = ""
|
||||
self._parHandle = None
|
||||
self._srcHandle = None
|
||||
self._srcItem = None
|
||||
|
||||
self._inFolder = False
|
||||
self._rawData = []
|
||||
|
||||
srcItem = self.theProject.tree[sHandle]
|
||||
if srcItem is not None and srcItem.isFileType():
|
||||
self._srcHandle = sHandle
|
||||
self._srcItem = srcItem
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def getError(self):
|
||||
"""Return any collected errors.
|
||||
"""
|
||||
return self._error
|
||||
|
||||
def setParentItem(self, pHandle):
|
||||
"""Set the item that will be the top level parent item for the
|
||||
new documents.
|
||||
"""
|
||||
self._parHandle = pHandle
|
||||
self._inFolder = False
|
||||
return
|
||||
|
||||
def newParentFolder(self, pHandle, folderLabel):
|
||||
"""Create a new folder that will be the top level parent item
|
||||
for the new documents.
|
||||
"""
|
||||
if self._srcItem is None:
|
||||
return None
|
||||
|
||||
newHandle = self.theProject.newFolder(folderLabel, pHandle)
|
||||
newItem = self.theProject.tree[newHandle]
|
||||
newItem.setStatus(self._srcItem.itemStatus)
|
||||
newItem.setImport(self._srcItem.itemImport)
|
||||
|
||||
self._parHandle = newHandle
|
||||
self._inFolder = True
|
||||
|
||||
return newHandle
|
||||
|
||||
def splitDocument(self, splitData, splitText):
|
||||
"""Loop through the split data record and perform the split job.
|
||||
"""
|
||||
self._rawData = []
|
||||
buffer = splitText.copy()
|
||||
for lineNo, hLevel, hLabel in reversed(splitData):
|
||||
chunk = buffer[lineNo:]
|
||||
buffer = buffer[:lineNo]
|
||||
self._rawData.insert(0, (chunk, hLevel, hLabel))
|
||||
|
||||
return True
|
||||
|
||||
def writeDocuments(self, docHierarchy):
|
||||
"""An iterator that will write each document in the buffer, and
|
||||
return its new handle, parent handle, and sibling handle.
|
||||
"""
|
||||
if self._srcHandle is None or self._srcItem is None:
|
||||
return
|
||||
|
||||
pHandle = self._parHandle
|
||||
nHandle = self._parHandle if self._inFolder else self._srcHandle
|
||||
hHandle = [self._parHandle, None, None, None, None]
|
||||
|
||||
pLevel = 0
|
||||
for docText, hLevel, docLabel in self._rawData:
|
||||
|
||||
hLevel = minmax(hLevel, 1, 4)
|
||||
if pLevel == 0:
|
||||
pLevel = hLevel
|
||||
|
||||
if docHierarchy:
|
||||
if hLevel == 1:
|
||||
pHandle = self._parHandle
|
||||
elif hLevel == 2:
|
||||
pHandle = hHandle[1] or hHandle[0]
|
||||
elif hLevel == 3:
|
||||
pHandle = hHandle[2] or hHandle[1] or hHandle[0]
|
||||
elif hLevel == 4:
|
||||
pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
|
||||
|
||||
if hLevel < pLevel:
|
||||
nHandle = hHandle[hLevel] or hHandle[0]
|
||||
elif hLevel > pLevel:
|
||||
nHandle = pHandle
|
||||
|
||||
dHandle = self.theProject.newFile(docLabel, pHandle)
|
||||
hHandle[hLevel] = dHandle
|
||||
|
||||
newItem = self.theProject.tree[dHandle]
|
||||
newItem.setStatus(self._srcItem.itemStatus)
|
||||
newItem.setImport(self._srcItem.itemImport)
|
||||
|
||||
outDoc = self.theProject.storage.getDocument(dHandle)
|
||||
status = outDoc.writeDocument("\n".join(docText))
|
||||
if not status:
|
||||
self._error = outDoc.getError()
|
||||
|
||||
yield status, dHandle, nHandle
|
||||
|
||||
hHandle[hLevel] = dHandle
|
||||
nHandle = dHandle
|
||||
pLevel = hLevel
|
||||
|
||||
return
|
||||
|
||||
# END Class DocSplitter
|
||||
|
||||
|
||||
class ProjectBuilder:
|
||||
"""A class to build a new project from a set of user-defined
|
||||
parameter provided by the New Projecty Wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, mainGui):
|
||||
|
||||
self.mainGui = mainGui
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
|
||||
self.tr = partial(QCoreApplication.translate, "NWProject")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def buildProject(self, data):
|
||||
"""Build a project from a data dictionary of specifications
|
||||
provided by the wizard.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
logger.error("Invalid call to newProject function")
|
||||
return False
|
||||
|
||||
popMinimal = data.get("popMinimal", True)
|
||||
popCustom = data.get("popCustom", False)
|
||||
popSample = data.get("popSample", False)
|
||||
|
||||
# Check if we're extracting the sample project. This is handled
|
||||
# differently as it isn't actually a new project, so we forward
|
||||
# this to another function and return here.
|
||||
if popSample:
|
||||
return self._extractSampleProject(data)
|
||||
|
||||
projPath = data.get("projPath", None)
|
||||
if projPath is None:
|
||||
logger.error("No project path set for the new project")
|
||||
return False
|
||||
|
||||
project = NWProject(self.mainGui)
|
||||
if not project.storage.openProjectInPlace(projPath, newProject=True):
|
||||
return False
|
||||
|
||||
lblNewProject = self.tr("New Project")
|
||||
lblNewChapter = self.tr("New Chapter")
|
||||
lblNewScene = self.tr("New Scene")
|
||||
lblTitlePage = self.tr("Title Page")
|
||||
lblByAuthors = self.tr("By")
|
||||
|
||||
# Settings
|
||||
projName = data.get("projName", lblNewProject)
|
||||
projTitle = data.get("projTitle", lblNewProject)
|
||||
projAuthors = data.get("projAuthors", "")
|
||||
|
||||
project.data.setUuid(None)
|
||||
project.data.setName(projName)
|
||||
project.data.setTitle(projTitle)
|
||||
project.data.setAuthors(projAuthors)
|
||||
project.setDefaultStatusImport()
|
||||
project._projOpened = int(time())
|
||||
|
||||
# Add Root Folders
|
||||
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
||||
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
|
||||
novelTitle = project.data.title if project.data.title else project.data.name
|
||||
|
||||
titlePage = f"#! {novelTitle}\n\n"
|
||||
if project.data.authors:
|
||||
titlePage += f">> {lblByAuthors} {project.getFormattedAuthors()} <<\n\n"
|
||||
|
||||
aDoc = project.storage.getDocument(hTitlePage)
|
||||
aDoc.writeDocument(titlePage)
|
||||
|
||||
if popMinimal:
|
||||
# Creating a minimal project with a few root folders and a
|
||||
# single chapter with a single scene.
|
||||
hChapter = project.newFile(lblNewChapter, hNovelRoot)
|
||||
aDoc = project.storage.getDocument(hChapter)
|
||||
aDoc.writeDocument(f"## {lblNewChapter}\n\n")
|
||||
|
||||
hScene = project.newFile(lblNewScene, hChapter)
|
||||
aDoc = project.storage.getDocument(hScene)
|
||||
aDoc.writeDocument(f"### {lblNewScene}\n\n")
|
||||
|
||||
project.newRoot(nwItemClass.PLOT)
|
||||
project.newRoot(nwItemClass.CHARACTER)
|
||||
project.newRoot(nwItemClass.WORLD)
|
||||
project.newRoot(nwItemClass.ARCHIVE)
|
||||
|
||||
project.saveProject()
|
||||
project.closeProject()
|
||||
|
||||
elif popCustom:
|
||||
# Create a project structure based on selected root folders
|
||||
# and a number of chapters and scenes selected in the
|
||||
# wizard's custom page.
|
||||
|
||||
# Create chapters and scenes
|
||||
numChapters = data.get("numChapters", 0)
|
||||
numScenes = data.get("numScenes", 0)
|
||||
|
||||
chSynop = self.tr("Summary of the chapter.")
|
||||
scSynop = self.tr("Summary of the scene.")
|
||||
|
||||
# Create chapters
|
||||
if numChapters > 0:
|
||||
for ch in range(numChapters):
|
||||
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
|
||||
cHandle = project.newFile(chTitle, hNovelRoot)
|
||||
aDoc = project.storage.getDocument(cHandle)
|
||||
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
|
||||
|
||||
# Create chapter scenes
|
||||
if numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
|
||||
sHandle = project.newFile(scTitle, cHandle)
|
||||
aDoc = project.storage.getDocument(sHandle)
|
||||
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
|
||||
|
||||
# Create scenes (no chapters)
|
||||
elif numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
|
||||
sHandle = project.newFile(scTitle, hNovelRoot)
|
||||
aDoc = project.storage.getDocument(sHandle)
|
||||
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
|
||||
|
||||
# Create notes folders
|
||||
noteTitles = {
|
||||
nwItemClass.PLOT: self.tr("Main Plot"),
|
||||
nwItemClass.CHARACTER: self.tr("Protagonist"),
|
||||
nwItemClass.WORLD: self.tr("Main Location"),
|
||||
}
|
||||
|
||||
addNotes = data.get("addNotes", False)
|
||||
for newRoot in data.get("addRoots", []):
|
||||
if newRoot in nwItemClass:
|
||||
rHandle = project.newRoot(newRoot)
|
||||
if addNotes:
|
||||
aHandle = project.newFile(noteTitles[newRoot], rHandle)
|
||||
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
|
||||
aDoc = project.storage.getDocument(aHandle)
|
||||
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
|
||||
|
||||
# Also add the archive and trash folders
|
||||
project.newRoot(nwItemClass.ARCHIVE)
|
||||
project.trashFolder()
|
||||
|
||||
project.saveProject()
|
||||
project.closeProject()
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _extractSampleProject(self, data):
|
||||
"""Make a copy of the sample project by extracting the
|
||||
sample.zip file to the new path.
|
||||
"""
|
||||
projPath = data.get("projPath", None)
|
||||
if projPath is None:
|
||||
logger.error("No project path set for the example project")
|
||||
return False
|
||||
|
||||
pkgSample = self.mainConf.assetPath("sample.zip")
|
||||
if pkgSample.is_file():
|
||||
try:
|
||||
shutil.unpack_archive(pkgSample, projPath)
|
||||
except Exception as exc:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Failed to create a new example project."
|
||||
), nwAlert.ERROR, exception=exc)
|
||||
return False
|
||||
|
||||
else:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Failed to create a new example project. "
|
||||
"Could not find the necessary files. "
|
||||
"They seem to be missing from this installation."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class ProjectBuilder
|
||||
@@ -23,9 +23,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||
from novelwriter.error import formatException
|
||||
from novelwriter.common import isHandle, sha256sum
|
||||
@@ -33,7 +34,7 @@ from novelwriter.common import isHandle, sha256sum
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWDoc():
|
||||
class NWDocument:
|
||||
|
||||
def __init__(self, theProject, theHandle):
|
||||
|
||||
@@ -57,7 +58,7 @@ class NWDoc():
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NWDoc handle={self._docHandle}>"
|
||||
return f"<NWDocument handle={self._docHandle}>"
|
||||
|
||||
def __bool__(self):
|
||||
return self._docHandle is not None and bool(self._theItem)
|
||||
@@ -73,7 +74,7 @@ class NWDoc():
|
||||
empty string. If something went wrong, return None.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
return None
|
||||
|
||||
@@ -81,17 +82,22 @@ class NWDoc():
|
||||
logger.error("Unknown novelWriter document")
|
||||
return None
|
||||
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
logger.error("No content path set")
|
||||
return None
|
||||
|
||||
docFile = self._docHandle+".nwd"
|
||||
logger.debug("Opening document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docPath = contentPath / docFile
|
||||
self._fileLoc = docPath
|
||||
|
||||
theText = ""
|
||||
self._docMeta = {}
|
||||
self._prevHash = None
|
||||
|
||||
if os.path.isfile(docPath):
|
||||
if docPath.exists():
|
||||
self._prevHash = sha256sum(docPath)
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -125,17 +131,20 @@ class NWDoc():
|
||||
if not.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
self.theProject.ensureFolderStructure()
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
docFile = self._docHandle+".nwd"
|
||||
logger.debug("Saving document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
||||
docPath = contentPath / docFile
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
if self._prevHash is not None and not forceWrite:
|
||||
self._currHash = sha256sum(docPath)
|
||||
@@ -164,7 +173,7 @@ class NWDoc():
|
||||
# If we're here, the file was successfully saved, so we can
|
||||
# replace the temp file with the actual file
|
||||
try:
|
||||
os.replace(docTemp, docPath)
|
||||
docTemp.replace(docPath)
|
||||
except OSError as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
@@ -179,23 +188,28 @@ class NWDoc():
|
||||
from the project data folder.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
chkList = [
|
||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
|
||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
|
||||
]
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
for chkFile in chkList:
|
||||
if os.path.isfile(chkFile):
|
||||
try:
|
||||
os.unlink(chkFile)
|
||||
logger.debug("Deleted: %s", chkFile)
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
docPath = contentPath / f"{self._docHandle}.nwd"
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
try:
|
||||
# ToDo: When Python 3.7 is dropped, these can be changed to
|
||||
# path.unlink(missing_ok=True)
|
||||
if docPath.exists():
|
||||
docPath.unlink()
|
||||
if docTemp.exists():
|
||||
docTemp.unlink()
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -206,7 +220,7 @@ class NWDoc():
|
||||
def getFileLocation(self):
|
||||
"""Return the file location of the current document.
|
||||
"""
|
||||
return self._fileLoc
|
||||
return str(self._fileLoc)
|
||||
|
||||
def getCurrentItem(self):
|
||||
"""Return a pointer to the currently open NWItem.
|
||||
@@ -263,4 +277,4 @@ class NWDoc():
|
||||
|
||||
return
|
||||
|
||||
# END Class NWDoc
|
||||
# END Class NWDocument
|
||||
|
||||
@@ -26,16 +26,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
|
||||
from novelwriter.core.document import NWDoc
|
||||
from novelwriter.common import (
|
||||
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
||||
)
|
||||
@@ -59,23 +58,23 @@ class NWIndex:
|
||||
The index data is cached in a JSON file between writing sessions.
|
||||
"""
|
||||
|
||||
def __init__(self, theProject):
|
||||
def __init__(self, project):
|
||||
|
||||
self.theProject = theProject
|
||||
self._project = project
|
||||
|
||||
# Storage and State
|
||||
self._tagsIndex = TagsIndex()
|
||||
self._itemIndex = ItemIndex(theProject)
|
||||
self._itemIndex = ItemIndex(project)
|
||||
self._indexBroken = False
|
||||
|
||||
# TimeStamps
|
||||
self._indexChange = 0
|
||||
self._indexChange = 0.0
|
||||
self._rootChange = {}
|
||||
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NWIndex project='{self.theProject.projName}'>"
|
||||
return f"<NWIndex project='{self._project.data.name}'>"
|
||||
|
||||
##
|
||||
# Properties
|
||||
@@ -94,10 +93,22 @@ class NWIndex:
|
||||
"""
|
||||
self._tagsIndex.clear()
|
||||
self._itemIndex.clear()
|
||||
self._indexChange = 0
|
||||
self._indexChange = 0.0
|
||||
self._rootChange = {}
|
||||
return
|
||||
|
||||
def rebuildIndex(self):
|
||||
"""Rebuild the entire index from scratch.
|
||||
"""
|
||||
self.clearIndex()
|
||||
for nwItem in self._project.tree:
|
||||
if nwItem is not None and nwItem.isFileType():
|
||||
tHandle = nwItem.itemHandle
|
||||
theDoc = self._project.storage.getDocument(tHandle)
|
||||
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||
self._indexBroken = False
|
||||
return
|
||||
|
||||
def deleteHandle(self, tHandle):
|
||||
"""Delete all entries of a given document handle.
|
||||
"""
|
||||
@@ -114,11 +125,11 @@ class NWIndex:
|
||||
moved from the archive or trash folders back into the active
|
||||
project.
|
||||
"""
|
||||
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
|
||||
if not self._project.tree.checkType(tHandle, nwItemType.FILE):
|
||||
return False
|
||||
|
||||
logger.debug("Re-indexing item '%s'", tHandle)
|
||||
theDoc = NWDoc(self.theProject, tHandle)
|
||||
theDoc = self._project.storage.getDocument(tHandle)
|
||||
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||
|
||||
return True
|
||||
@@ -126,13 +137,13 @@ class NWIndex:
|
||||
def indexChangedSince(self, checkTime):
|
||||
"""Check if the index has changed since a given time.
|
||||
"""
|
||||
return self._indexChange > checkTime
|
||||
return self._indexChange > float(checkTime)
|
||||
|
||||
def rootChangedSince(self, rootHandle, checkTime):
|
||||
"""Check if the index has changed since a given time for a
|
||||
given root item.
|
||||
"""
|
||||
return self._rootChange.get(rootHandle, self._indexChange) > checkTime
|
||||
return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
|
||||
|
||||
##
|
||||
# Load and Save Index to/from File
|
||||
@@ -141,12 +152,15 @@ class NWIndex:
|
||||
def loadIndex(self):
|
||||
"""Load index from last session from the project meta folder.
|
||||
"""
|
||||
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||
if not isinstance(indexFile, Path):
|
||||
return False
|
||||
|
||||
theData = {}
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
tStart = time()
|
||||
|
||||
self._indexBroken = False
|
||||
if os.path.isfile(indexFile):
|
||||
if indexFile.exists():
|
||||
logger.debug("Loading index file")
|
||||
try:
|
||||
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -169,14 +183,14 @@ class NWIndex:
|
||||
logger.debug("Checking index")
|
||||
|
||||
# Check that all files are indexed
|
||||
for fHandle in self.theProject.projFiles:
|
||||
for fHandle in self._project.projFiles:
|
||||
if fHandle not in self._itemIndex:
|
||||
logger.warning("Item '%s' is not in the index", fHandle)
|
||||
self.reIndexHandle(fHandle)
|
||||
|
||||
self._indexChange = round(time())
|
||||
self._indexChange = time()
|
||||
|
||||
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
|
||||
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return True
|
||||
|
||||
@@ -184,8 +198,11 @@ class NWIndex:
|
||||
"""Save the current index as a json file in the project meta
|
||||
data folder.
|
||||
"""
|
||||
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||
if not isinstance(indexFile, Path):
|
||||
return False
|
||||
|
||||
logger.debug("Saving index file")
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
tStart = time()
|
||||
|
||||
try:
|
||||
@@ -202,7 +219,7 @@ class NWIndex:
|
||||
logException()
|
||||
return False
|
||||
|
||||
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000)
|
||||
logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return True
|
||||
|
||||
@@ -217,11 +234,11 @@ class NWIndex:
|
||||
files before we save them, in which case we already have the
|
||||
text.
|
||||
"""
|
||||
theItem = self.theProject.tree[tHandle]
|
||||
theItem = self._project.tree[tHandle]
|
||||
if theItem is None:
|
||||
logger.info("Not indexing unknown item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
if not theItem.isFileType():
|
||||
logger.info("Not indexing non-file item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
@@ -243,20 +260,43 @@ class NWIndex:
|
||||
if theItem.itemParent is None:
|
||||
logger.info("Not indexing orphaned item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.isInactive():
|
||||
logger.debug("Not indexing inactive item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
logger.debug("Indexing item with handle '%s'", tHandle)
|
||||
if theItem.isInactive():
|
||||
self._scanInactive(theItem, theText)
|
||||
else:
|
||||
self._scanActive(tHandle, theItem, theText, itemTags)
|
||||
|
||||
# Scan the text content
|
||||
# Update timestamps for index changes
|
||||
nowTime = time()
|
||||
self._indexChange = nowTime
|
||||
self._rootChange[theItem.itemRoot] = nowTime
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Indexer Helpers
|
||||
##
|
||||
|
||||
def _scanActive(self, tHandle, theItem, theText, itemTags):
|
||||
"""Scan an active document for meta data.
|
||||
"""
|
||||
nTitle = 0
|
||||
findHeader = True
|
||||
theLines = theText.splitlines()
|
||||
|
||||
for nLine, aLine in enumerate(theLines, start=1):
|
||||
|
||||
if len(aLine.strip()) == 0:
|
||||
continue
|
||||
|
||||
if aLine.startswith("#"):
|
||||
if findHeader:
|
||||
hDepth, _ = self._splitHeading(aLine)
|
||||
if hDepth != "H0":
|
||||
theItem.setMainHeading(hDepth)
|
||||
findHeader = False
|
||||
|
||||
isTitle = self._indexTitle(tHandle, aLine, nLine)
|
||||
if isTitle and nLine > 0:
|
||||
if nTitle > 0:
|
||||
@@ -289,48 +329,49 @@ class NWIndex:
|
||||
# Prune no longer used tags
|
||||
for tTag, isActive in itemTags.items():
|
||||
if not isActive:
|
||||
logger.verbose("Deleting removed tag '%s'", tTag)
|
||||
logger.debug("Deleting removed tag '%s'", tTag)
|
||||
del self._tagsIndex[tTag]
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = round(time())
|
||||
self._indexChange = nowTime
|
||||
self._rootChange[theItem.itemRoot] = nowTime
|
||||
return
|
||||
|
||||
return True
|
||||
def _scanInactive(self, theItem, theText):
|
||||
"""Scan an inactive document for meta data.
|
||||
"""
|
||||
for aLine in theText.splitlines():
|
||||
if aLine.startswith("#"):
|
||||
hDepth, _ = self._splitHeading(aLine)
|
||||
if hDepth != "H0":
|
||||
theItem.setMainHeading(hDepth)
|
||||
break
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Indexer Helpers
|
||||
##
|
||||
def _splitHeading(self, aLine):
|
||||
"""Split a heading into its header level and text value.
|
||||
"""
|
||||
if aLine.startswith("# "):
|
||||
return "H1", aLine[2:].strip()
|
||||
elif aLine.startswith("## "):
|
||||
return "H2", aLine[3:].strip()
|
||||
elif aLine.startswith("### "):
|
||||
return "H3", aLine[4:].strip()
|
||||
elif aLine.startswith("#### "):
|
||||
return "H4", aLine[5:].strip()
|
||||
elif aLine.startswith("#! "):
|
||||
return "H1", aLine[3:].strip()
|
||||
elif aLine.startswith("##! "):
|
||||
return "H2", aLine[4:].strip()
|
||||
return "H0", ""
|
||||
|
||||
def _indexTitle(self, tHandle, aLine, nTitle):
|
||||
"""Save information about the title and its location in the
|
||||
file to the index.
|
||||
"""
|
||||
if aLine.startswith("# "):
|
||||
hDepth = "H1"
|
||||
hText = aLine[2:].strip()
|
||||
elif aLine.startswith("## "):
|
||||
hDepth = "H2"
|
||||
hText = aLine[3:].strip()
|
||||
elif aLine.startswith("### "):
|
||||
hDepth = "H3"
|
||||
hText = aLine[4:].strip()
|
||||
elif aLine.startswith("#### "):
|
||||
hDepth = "H4"
|
||||
hText = aLine[5:].strip()
|
||||
elif aLine.startswith("#! "):
|
||||
hDepth = "H1"
|
||||
hText = aLine[3:].strip()
|
||||
elif aLine.startswith("##! "):
|
||||
hDepth = "H2"
|
||||
hText = aLine[4:].strip()
|
||||
else:
|
||||
hDepth, hText = self._splitHeading(aLine)
|
||||
if hDepth == "H0":
|
||||
return False
|
||||
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
|
||||
|
||||
return True
|
||||
|
||||
def _indexWordCounts(self, tHandle, theText, nTitle):
|
||||
@@ -493,18 +534,15 @@ class NWIndex:
|
||||
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
||||
]
|
||||
|
||||
def getHandleHeaderLevel(self, tHandle):
|
||||
"""Get the header level of the first header of a handle.
|
||||
"""
|
||||
return self._itemIndex.mainItemHeader(tHandle)
|
||||
|
||||
def getTableOfContents(self, maxDepth, skipExcl=True):
|
||||
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
|
||||
"""Generate a table of contents up to a maximum depth.
|
||||
"""
|
||||
tOrder = []
|
||||
tData = {}
|
||||
pKey = None
|
||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
|
||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
|
||||
rootHandle=rootHandle, skipExcl=skipExcl
|
||||
):
|
||||
tKey = f"{tHandle}:{sTitle}"
|
||||
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
||||
if iLevel > maxDepth:
|
||||
@@ -647,23 +685,17 @@ class TagsIndex:
|
||||
def tagHandle(self, tagKey):
|
||||
"""Get the handle of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("handle")
|
||||
return None
|
||||
return self._tags.get(tagKey, {}).get("handle", None)
|
||||
|
||||
def tagHeading(self, tagKey):
|
||||
"""Get the heading of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("heading")
|
||||
return nwHeaders.TT_NONE
|
||||
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
|
||||
|
||||
def tagClass(self, tagKey):
|
||||
"""Get the class of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("class")
|
||||
return None
|
||||
return self._tags.get(tagKey, {}).get("class", None)
|
||||
|
||||
##
|
||||
# Pack/Unpack
|
||||
@@ -717,8 +749,8 @@ class ItemIndex:
|
||||
IndexHeading object for each header of the text.
|
||||
"""
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
def __init__(self, project):
|
||||
self._project = project
|
||||
self._items = {}
|
||||
return
|
||||
|
||||
@@ -755,13 +787,6 @@ class ItemIndex:
|
||||
self._items[tHandle] = IndexItem(tHandle, tItem)
|
||||
return
|
||||
|
||||
def mainItemHeader(self, tHandle):
|
||||
"""Return the primary item header for an item.
|
||||
"""
|
||||
if tHandle in self._items:
|
||||
return self._items[tHandle].level
|
||||
return "H0"
|
||||
|
||||
def allItemTags(self, tHandle):
|
||||
"""Get all tags set for headings of an item.
|
||||
"""
|
||||
@@ -789,12 +814,12 @@ class ItemIndex:
|
||||
"""Iterate over all items and headers in the novel structure for
|
||||
a given root handle, or for all if root handle is None.
|
||||
"""
|
||||
for tItem in self.theProject.tree:
|
||||
for tItem in self._project.tree:
|
||||
if tItem is None:
|
||||
continue
|
||||
if tItem.itemLayout == nwItemLayout.NOTE:
|
||||
if tItem.isNoteLayout():
|
||||
continue
|
||||
if skipExcl and not tItem.isExported:
|
||||
if skipExcl and not tItem.isActive:
|
||||
continue
|
||||
|
||||
tHandle = tItem.itemHandle
|
||||
@@ -807,8 +832,6 @@ class ItemIndex:
|
||||
elif tItem.itemRoot == rootHandle:
|
||||
for sTitle in self._items[tHandle].headings():
|
||||
yield tHandle, sTitle, self._items[tHandle][sTitle]
|
||||
else:
|
||||
continue
|
||||
|
||||
return
|
||||
|
||||
@@ -821,7 +844,6 @@ class ItemIndex:
|
||||
"""
|
||||
if tHandle in self._items:
|
||||
tItem = self._items[tHandle]
|
||||
tItem.updateLevel(hDepth)
|
||||
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
|
||||
return
|
||||
|
||||
@@ -875,7 +897,7 @@ class ItemIndex:
|
||||
if not isHandle(tHandle):
|
||||
raise ValueError("itemIndex keys must be handles")
|
||||
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
nwItem = self._project.tree[tHandle]
|
||||
if nwItem is not None:
|
||||
tItem = IndexItem(tHandle, nwItem)
|
||||
tItem.unpackData(tData)
|
||||
@@ -897,7 +919,6 @@ class IndexItem:
|
||||
def __init__(self, tHandle, tItem):
|
||||
self._handle = tHandle
|
||||
self._item = tItem
|
||||
self._level = "H0"
|
||||
self._headings = {}
|
||||
self._index = 0
|
||||
|
||||
@@ -917,21 +938,10 @@ class IndexItem:
|
||||
def item(self):
|
||||
return self._item
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return self._level
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def updateLevel(self, level):
|
||||
"""Set the level only if it has not already been set.
|
||||
"""
|
||||
if self._level == "H0":
|
||||
self._level = level
|
||||
return
|
||||
|
||||
def addHeading(self, tHeading):
|
||||
"""Add a heading to the item. Also remove the placeholder entry
|
||||
if it exists.
|
||||
@@ -1011,7 +1021,7 @@ class IndexItem:
|
||||
if hRefs:
|
||||
refs[sTitle] = hRefs
|
||||
|
||||
data = {"level": self._level}
|
||||
data = {}
|
||||
data["headings"] = heads
|
||||
if refs:
|
||||
data["references"] = refs
|
||||
@@ -1021,7 +1031,6 @@ class IndexItem:
|
||||
def unpackData(self, data):
|
||||
"""Unpack an item entry from the data.
|
||||
"""
|
||||
self._level = data.get("level", "H0")
|
||||
references = data.get("references", {})
|
||||
for sTitle, hData in data.get("headings", {}).items():
|
||||
if not isTitleTag(sTitle):
|
||||
@@ -1030,6 +1039,7 @@ class IndexItem:
|
||||
tHeading.unpackData(hData)
|
||||
tHeading.unpackReferences(references.get(sTitle, {}))
|
||||
self.addHeading(tHeading)
|
||||
|
||||
return
|
||||
|
||||
# END Class IndexItem
|
||||
|
||||
@@ -25,23 +25,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from novelwriter.common import (
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
|
||||
)
|
||||
from novelwriter.constants import nwLabels, trConst
|
||||
from novelwriter.constants import nwHeaders, nwLabels, trConst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWItem():
|
||||
class NWItem:
|
||||
|
||||
def __init__(self, theProject):
|
||||
__slots__ = (
|
||||
"_project", "_name", "_handle", "_parent", "_root", "_order",
|
||||
"_type", "_class", "_layout", "_status", "_import", "_active",
|
||||
"_expanded", "_heading", "_charCount", "_wordCount",
|
||||
"_paraCount", "_cursorPos", "_initCount",
|
||||
)
|
||||
|
||||
self.theProject = theProject
|
||||
def __init__(self, project):
|
||||
|
||||
self._project = project
|
||||
self._name = ""
|
||||
self._handle = None
|
||||
self._parent = None
|
||||
@@ -52,15 +56,16 @@ class NWItem():
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
self._status = None
|
||||
self._import = None
|
||||
self._active = True
|
||||
self._expanded = False
|
||||
self._exported = True
|
||||
|
||||
# Document Meta Data
|
||||
self._charCount = 0 # Current character count
|
||||
self._wordCount = 0 # Current word count
|
||||
self._paraCount = 0 # Current paragraph count
|
||||
self._cursorPos = 0 # Last cursor position
|
||||
self._initCount = 0 # Initial word count
|
||||
self._heading = "H0" # The main heading
|
||||
self._charCount = 0 # Current character count
|
||||
self._wordCount = 0 # Current word count
|
||||
self._paraCount = 0 # Current paragraph count
|
||||
self._cursorPos = 0 # Last cursor position
|
||||
self._initCount = 0 # Initial word count
|
||||
|
||||
return
|
||||
|
||||
@@ -114,13 +119,17 @@ class NWItem():
|
||||
def itemImport(self):
|
||||
return self._import
|
||||
|
||||
@property
|
||||
def isActive(self):
|
||||
return self._active
|
||||
|
||||
@property
|
||||
def isExpanded(self):
|
||||
return self._expanded
|
||||
|
||||
@property
|
||||
def isExported(self):
|
||||
return self._exported
|
||||
def mainHeading(self):
|
||||
return self._heading
|
||||
|
||||
@property
|
||||
def charCount(self):
|
||||
@@ -143,101 +152,75 @@ class NWItem():
|
||||
return self._cursorPos
|
||||
|
||||
##
|
||||
# XML Pack/Unpack
|
||||
# Pack/Unpack Data
|
||||
##
|
||||
|
||||
def packXML(self, xParent):
|
||||
"""Pack all the data in the class instance into an XML object.
|
||||
def pack(self):
|
||||
"""Pack all the data in the class instance into a dictionary.
|
||||
"""
|
||||
itemAttrib = {}
|
||||
itemAttrib["handle"] = str(self._handle)
|
||||
itemAttrib["parent"] = str(self._parent)
|
||||
itemAttrib["root"] = str(self._root)
|
||||
itemAttrib["order"] = str(self._order)
|
||||
itemAttrib["type"] = str(self._type.name)
|
||||
itemAttrib["class"] = str(self._class.name)
|
||||
item = {}
|
||||
meta = {}
|
||||
name = {}
|
||||
|
||||
item["handle"] = str(self._handle)
|
||||
item["parent"] = str(self._parent)
|
||||
item["root"] = str(self._root)
|
||||
item["order"] = str(self._order)
|
||||
item["type"] = str(self._type.name)
|
||||
item["class"] = str(self._class.name)
|
||||
meta["expanded"] = yesNo(self._expanded)
|
||||
name["status"] = str(self._status)
|
||||
name["import"] = str(self._import)
|
||||
|
||||
if self._type == nwItemType.FILE:
|
||||
itemAttrib["layout"] = str(self._layout.name)
|
||||
item["layout"] = str(self._layout.name)
|
||||
meta["heading"] = str(self._heading)
|
||||
meta["charCount"] = str(self._charCount)
|
||||
meta["wordCount"] = str(self._wordCount)
|
||||
meta["paraCount"] = str(self._paraCount)
|
||||
meta["cursorPos"] = str(self._cursorPos)
|
||||
name["active"] = yesNo(self._active)
|
||||
|
||||
metaAttrib = {}
|
||||
metaAttrib["expanded"] = str(self._expanded)
|
||||
if self._type == nwItemType.FILE:
|
||||
metaAttrib["charCount"] = str(self._charCount)
|
||||
metaAttrib["wordCount"] = str(self._wordCount)
|
||||
metaAttrib["paraCount"] = str(self._paraCount)
|
||||
metaAttrib["cursorPos"] = str(self._cursorPos)
|
||||
data = {
|
||||
"name": str(self._name),
|
||||
"itemAttr": item,
|
||||
"metaAttr": meta,
|
||||
"nameAttr": name,
|
||||
}
|
||||
|
||||
nameAttrib = {}
|
||||
nameAttrib["status"] = str(self._status)
|
||||
nameAttrib["import"] = str(self._import)
|
||||
if self._type == nwItemType.FILE:
|
||||
nameAttrib["exported"] = str(self._exported)
|
||||
return data
|
||||
|
||||
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
|
||||
self._subPack(xPack, "meta", attrib=metaAttrib)
|
||||
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
|
||||
|
||||
return
|
||||
|
||||
def unpackXML(self, xItem):
|
||||
"""Set the values from an XML entry of type 'item'.
|
||||
def unpack(self, data):
|
||||
"""Set the values from a data dictionary.
|
||||
"""
|
||||
if xItem.tag != "item":
|
||||
logger.error("XML entry is not an NWItem")
|
||||
return False
|
||||
item = data.get("itemAttr", {})
|
||||
meta = data.get("metaAttr", {})
|
||||
name = data.get("nameAttr", {})
|
||||
|
||||
if "handle" in xItem.attrib:
|
||||
self.setHandle(xItem.attrib["handle"])
|
||||
if "handle" in item:
|
||||
self.setHandle(item["handle"])
|
||||
else:
|
||||
logger.error("XML item entry does not have a handle")
|
||||
logger.error("Item does not have a handle")
|
||||
return False
|
||||
|
||||
self.setParent(xItem.attrib.get("parent", None))
|
||||
self.setRoot(xItem.attrib.get("root", None))
|
||||
self.setOrder(xItem.attrib.get("order", 0))
|
||||
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
|
||||
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
|
||||
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
|
||||
self.setName(data.get("name", ""))
|
||||
self.setParent(item.get("parent", None))
|
||||
self.setRoot(item.get("root", None))
|
||||
self.setOrder(item.get("order", 0))
|
||||
self.setType(item.get("type", nwItemType.NO_TYPE))
|
||||
self.setClass(item.get("class", nwItemClass.NO_CLASS))
|
||||
self.setExpanded(meta.get("expanded", False))
|
||||
self.setStatus(name.get("status", None))
|
||||
self.setImport(name.get("import", None))
|
||||
|
||||
for xValue in xItem:
|
||||
if xValue.tag == "meta":
|
||||
self.setExpanded(xValue.attrib.get("expanded", False))
|
||||
self.setCharCount(xValue.attrib.get("charCount", 0))
|
||||
self.setWordCount(xValue.attrib.get("wordCount", 0))
|
||||
self.setParaCount(xValue.attrib.get("paraCount", 0))
|
||||
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
|
||||
elif xValue.tag == "name":
|
||||
self.setName(xValue.text)
|
||||
self.setStatus(xValue.attrib.get("status", None))
|
||||
self.setImport(xValue.attrib.get("import", None))
|
||||
self.setExported(xValue.attrib.get("exported", True))
|
||||
|
||||
# Legacy Format (1.3 and earlier)
|
||||
elif xValue.tag == "status":
|
||||
self.setImportStatus(xValue.text)
|
||||
elif xValue.tag == "type":
|
||||
self.setType(xValue.text)
|
||||
elif xValue.tag == "class":
|
||||
self.setClass(xValue.text)
|
||||
elif xValue.tag == "layout":
|
||||
self.setLayout(xValue.text)
|
||||
elif xValue.tag == "expanded":
|
||||
self.setExpanded(xValue.text)
|
||||
elif xValue.tag == "exported":
|
||||
self.setExported(xValue.text)
|
||||
elif xValue.tag == "charCount":
|
||||
self.setCharCount(xValue.text)
|
||||
elif xValue.tag == "wordCount":
|
||||
self.setWordCount(xValue.text)
|
||||
elif xValue.tag == "paraCount":
|
||||
self.setParaCount(xValue.text)
|
||||
elif xValue.tag == "cursorPos":
|
||||
self.setCursorPos(xValue.text)
|
||||
else:
|
||||
# Sliently skip as we may otherwise cause orphaned
|
||||
# items if an otherwise valid file is opened by a
|
||||
# version of novelWriter that doesn't know the tag
|
||||
logger.error("Unknown tag '%s'", xValue.tag)
|
||||
if self._type == nwItemType.FILE:
|
||||
self.setLayout(item.get("layout", nwItemLayout.NO_LAYOUT))
|
||||
self.setMainHeading(meta.get("heading", "H0"))
|
||||
self.setCharCount(meta.get("charCount", 0))
|
||||
self.setWordCount(meta.get("wordCount", 0))
|
||||
self.setParaCount(meta.get("paraCount", 0))
|
||||
self.setCursorPos(meta.get("cursorPos", 0))
|
||||
self.setActive(name.get("active", True))
|
||||
|
||||
# Make some checks to ensure consistency
|
||||
if self._type == nwItemType.ROOT:
|
||||
@@ -245,31 +228,22 @@ class NWItem():
|
||||
self._parent = None # Root items cannot have a parent
|
||||
|
||||
if self._type != nwItemType.FILE:
|
||||
self._charCount = 0 # Only set for files
|
||||
self._wordCount = 0 # Only set for files
|
||||
self._paraCount = 0 # Only set for files
|
||||
self._cursorPos = 0 # Only set for files
|
||||
# Reset values that should only be set for files
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
self._heading = "H0"
|
||||
self._active = False
|
||||
self._charCount = 0
|
||||
self._wordCount = 0
|
||||
self._paraCount = 0
|
||||
self._cursorPos = 0
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _subPack(xParent, name, attrib=None, text=None, none=True):
|
||||
"""Pack the values into an XML element.
|
||||
"""
|
||||
if not none and (text is None or text == "None"):
|
||||
return None
|
||||
xAttr = {} if attrib is None else attrib
|
||||
xSub = etree.SubElement(xParent, name, attrib=xAttr)
|
||||
if text is not None:
|
||||
xSub.text = text
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Lookup Methods
|
||||
##
|
||||
|
||||
def describeMe(self, hLevel=None):
|
||||
def describeMe(self):
|
||||
"""Return a string description of the item.
|
||||
"""
|
||||
descKey = "none"
|
||||
@@ -279,12 +253,14 @@ class NWItem():
|
||||
descKey = "folder"
|
||||
elif self._type == nwItemType.FILE:
|
||||
if self._layout == nwItemLayout.DOCUMENT:
|
||||
if hLevel == "H1":
|
||||
if self._heading == "H1":
|
||||
descKey = "doc_h1"
|
||||
elif hLevel == "H2":
|
||||
elif self._heading == "H2":
|
||||
descKey = "doc_h2"
|
||||
elif hLevel == "H3":
|
||||
elif self._heading == "H3":
|
||||
descKey = "doc_h3"
|
||||
elif self._heading == "H4":
|
||||
descKey = "doc_h4"
|
||||
else:
|
||||
descKey = "document"
|
||||
elif self._layout == nwItemLayout.NOTE:
|
||||
@@ -292,6 +268,22 @@ class NWItem():
|
||||
|
||||
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
|
||||
|
||||
def getImportStatus(self, incIcon=True):
|
||||
"""Return the relevant importance or status label and icon for
|
||||
the current item based on its class.
|
||||
"""
|
||||
if self.isNovelLike():
|
||||
stName = self._project.data.itemStatus.name(self._status)
|
||||
stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None
|
||||
else:
|
||||
stName = self._project.data.itemImport.name(self._import)
|
||||
stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None
|
||||
return stName, stIcon
|
||||
|
||||
##
|
||||
# Checker Methods
|
||||
##
|
||||
|
||||
def isNovelLike(self):
|
||||
"""Returns true if the item is of a novel-like class.
|
||||
"""
|
||||
@@ -307,17 +299,20 @@ class NWItem():
|
||||
"""
|
||||
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
|
||||
|
||||
def getImportStatus(self):
|
||||
"""Return the relevant importance or status label and icon for
|
||||
the current item based on its class.
|
||||
"""
|
||||
if self.isNovelLike():
|
||||
stName = self.theProject.statusItems.name(self._status)
|
||||
stIcon = self.theProject.statusItems.icon(self._status)
|
||||
else:
|
||||
stName = self.theProject.importItems.name(self._import)
|
||||
stIcon = self.theProject.importItems.icon(self._import)
|
||||
return stName, stIcon
|
||||
def isRootType(self):
|
||||
return self._type == nwItemType.ROOT
|
||||
|
||||
def isFolderType(self):
|
||||
return self._type == nwItemType.FOLDER
|
||||
|
||||
def isFileType(self):
|
||||
return self._type == nwItemType.FILE
|
||||
|
||||
def isNoteLayout(self):
|
||||
return self._layout == nwItemLayout.NOTE
|
||||
|
||||
def isDocumentLayout(self):
|
||||
return self._layout == nwItemLayout.DOCUMENT
|
||||
|
||||
##
|
||||
# Special Setters
|
||||
@@ -419,8 +414,6 @@ class NWItem():
|
||||
self._type = value
|
||||
elif isItemType(value):
|
||||
self._type = nwItemType[value]
|
||||
elif value == "TRASH":
|
||||
self._type = nwItemType.ROOT
|
||||
else:
|
||||
logger.error("Unrecognised item type '%s'", value)
|
||||
self._type = nwItemType.NO_TYPE
|
||||
@@ -447,8 +440,6 @@ class NWItem():
|
||||
self._layout = value
|
||||
elif isItemLayout(value):
|
||||
self._layout = nwItemLayout[value]
|
||||
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
|
||||
self._layout = nwItemLayout.DOCUMENT
|
||||
else:
|
||||
logger.error("Unrecognised item layout '%s'", value)
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
@@ -458,60 +449,79 @@ class NWItem():
|
||||
"""Set the item status by looking it up in the valid status
|
||||
items of the current project.
|
||||
"""
|
||||
self._status = self.theProject.statusItems.check(value)
|
||||
self._status = self._project.data.itemStatus.check(value)
|
||||
return
|
||||
|
||||
def setImport(self, value):
|
||||
"""Set the item importance by looking it up in the valid import
|
||||
items of the current project.
|
||||
"""
|
||||
self._import = self.theProject.importItems.check(value)
|
||||
self._import = self._project.data.itemImport.check(value)
|
||||
return
|
||||
|
||||
def setActive(self, state):
|
||||
"""Set the active flag.
|
||||
"""
|
||||
if isinstance(state, bool):
|
||||
self._active = state
|
||||
else:
|
||||
self._active = False
|
||||
return
|
||||
|
||||
def setExpanded(self, state):
|
||||
"""Set the expanded status of an item in the project tree.
|
||||
"""
|
||||
if isinstance(state, str):
|
||||
self._expanded = (state == str(True))
|
||||
if isinstance(state, bool):
|
||||
self._expanded = state
|
||||
else:
|
||||
self._expanded = (state is True)
|
||||
return
|
||||
|
||||
def setExported(self, state):
|
||||
"""Set the export flag.
|
||||
"""
|
||||
if isinstance(state, str):
|
||||
self._exported = (state == str(True))
|
||||
else:
|
||||
self._exported = (state is True)
|
||||
self._expanded = False
|
||||
return
|
||||
|
||||
##
|
||||
# Set Document Meta Data
|
||||
##
|
||||
|
||||
def setMainHeading(self, value):
|
||||
"""Set the main heading level.
|
||||
"""
|
||||
if value in nwHeaders.H_LEVEL:
|
||||
self._heading = value
|
||||
return
|
||||
|
||||
def setCharCount(self, count):
|
||||
"""Set the character count, and ensure that it is an integer.
|
||||
"""
|
||||
self._charCount = max(0, checkInt(count, 0))
|
||||
if isinstance(count, int):
|
||||
self._charCount = max(0, count)
|
||||
else:
|
||||
self._charCount = 0
|
||||
return
|
||||
|
||||
def setWordCount(self, count):
|
||||
"""Set the word count, and ensure that it is an integer.
|
||||
"""
|
||||
self._wordCount = max(0, checkInt(count, 0))
|
||||
if isinstance(count, int):
|
||||
self._wordCount = max(0, count)
|
||||
else:
|
||||
self._wordCount = 0
|
||||
return
|
||||
|
||||
def setParaCount(self, count):
|
||||
"""Set the paragraph count, and ensure that it is an integer.
|
||||
"""
|
||||
self._paraCount = max(0, checkInt(count, 0))
|
||||
if isinstance(count, int):
|
||||
self._paraCount = max(0, count)
|
||||
else:
|
||||
self._paraCount = 0
|
||||
return
|
||||
|
||||
def setCursorPos(self, position):
|
||||
"""Set the cursor position, and ensure that it is an integer.
|
||||
"""
|
||||
self._cursorPos = max(0, checkInt(position, 0))
|
||||
if isinstance(position, int):
|
||||
self._cursorPos = max(0, position)
|
||||
else:
|
||||
self._cursorPos = 0
|
||||
return
|
||||
|
||||
def saveInitialCount(self):
|
||||
|
||||
@@ -24,11 +24,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
|
||||
@@ -40,30 +40,30 @@ VALID_MAP = {
|
||||
"GuiWritingStats": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
|
||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
|
||||
},
|
||||
"GuiDocSplit": {"spLevel"},
|
||||
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
|
||||
"GuiBuildNovel": {
|
||||
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
||||
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
||||
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
|
||||
"incSynopsis", "incComments", "incKeywords", "incBodyText",
|
||||
"replaceTabs", "replaceUCode"
|
||||
"replaceTabs", "replaceUCode", "rootFilter",
|
||||
},
|
||||
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
|
||||
"GuiProjectSettings": {
|
||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
|
||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
||||
},
|
||||
"GuiWordList": {"winWidth", "winHeight"},
|
||||
"GuiNovelView": {"lastCol"},
|
||||
}
|
||||
|
||||
|
||||
class OptionState():
|
||||
class OptionState:
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
@@ -77,13 +77,12 @@ class OptionState():
|
||||
def loadSettings(self):
|
||||
"""Load the options dictionary from the project settings file.
|
||||
"""
|
||||
if self.theProject.projMeta is None:
|
||||
stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||
if not isinstance(stateFile, Path):
|
||||
return False
|
||||
|
||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
||||
theState = {}
|
||||
|
||||
if os.path.isfile(stateFile):
|
||||
if stateFile.exists():
|
||||
logger.debug("Loading GUI options file")
|
||||
try:
|
||||
with open(stateFile, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -106,12 +105,11 @@ class OptionState():
|
||||
def saveSettings(self):
|
||||
"""Save the options dictionary to the project settings file.
|
||||
"""
|
||||
if self.theProject.projMeta is None:
|
||||
stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||
if not isinstance(stateFile, Path):
|
||||
return False
|
||||
|
||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
||||
logger.debug("Saving GUI options file")
|
||||
|
||||
try:
|
||||
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump(self._theState, outFile, indent=2)
|
||||
@@ -188,8 +186,7 @@ class OptionState():
|
||||
the default value.
|
||||
"""
|
||||
if group in self._theState:
|
||||
if name in self._theState[group]:
|
||||
return checkBool(self._theState[group].get(name, default), default)
|
||||
return checkBool(self._theState[group].get(name, default), default)
|
||||
return default
|
||||
|
||||
def getEnum(self, group, name, lookup, default):
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
novelWriter – Project Data Class
|
||||
================================
|
||||
Data class for novelWriter projects
|
||||
|
||||
File History:
|
||||
Created: 2022-10-30 [2.0rc1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from novelwriter.common import (
|
||||
checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified
|
||||
)
|
||||
from novelwriter.core.status import NWStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWProjectData:
|
||||
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.theProject = theProject
|
||||
|
||||
# Project Meta
|
||||
self._uuid = ""
|
||||
self._name = ""
|
||||
self._title = ""
|
||||
self._authors = []
|
||||
self._saveCount = 0
|
||||
self._autoCount = 0
|
||||
self._editTime = 0
|
||||
|
||||
# Project Settings
|
||||
self._doBackup = True
|
||||
self._language = None
|
||||
self._spellCheck = False
|
||||
self._spellLang = None
|
||||
|
||||
# Project Dictionaries
|
||||
self._initCounts = [0, 0]
|
||||
self._currCounts = [0, 0]
|
||||
self._lastHandle: dict[str, str | None] = {
|
||||
"editor": None,
|
||||
"viewer": None,
|
||||
"novelTree": None,
|
||||
"outline": None,
|
||||
}
|
||||
self._autoReplace: dict[str, str] = {}
|
||||
self._titleFormat: dict[str, str] = {
|
||||
"title": "%title%",
|
||||
"chapter": "%title%",
|
||||
"unnumbered": "%title%",
|
||||
"scene": "* * *",
|
||||
"section": "",
|
||||
}
|
||||
|
||||
self._status = NWStatus(NWStatus.STATUS)
|
||||
self._import = NWStatus(NWStatus.IMPORT)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def authors(self):
|
||||
return self._authors
|
||||
|
||||
@property
|
||||
def saveCount(self):
|
||||
return self._saveCount
|
||||
|
||||
@property
|
||||
def autoCount(self):
|
||||
return self._autoCount
|
||||
|
||||
@property
|
||||
def editTime(self):
|
||||
return self._editTime
|
||||
|
||||
@property
|
||||
def doBackup(self):
|
||||
return self._doBackup
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return self._language
|
||||
|
||||
@property
|
||||
def spellCheck(self):
|
||||
return self._spellCheck
|
||||
|
||||
@property
|
||||
def spellLang(self):
|
||||
return self._spellLang
|
||||
|
||||
@property
|
||||
def initCounts(self):
|
||||
return tuple(self._initCounts)
|
||||
|
||||
@property
|
||||
def currCounts(self):
|
||||
return tuple(self._currCounts)
|
||||
|
||||
@property
|
||||
def lastHandle(self):
|
||||
return self._lastHandle
|
||||
|
||||
@property
|
||||
def autoReplace(self):
|
||||
return self._autoReplace
|
||||
|
||||
@property
|
||||
def titleFormat(self):
|
||||
return self._titleFormat
|
||||
|
||||
@property
|
||||
def itemStatus(self):
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def itemImport(self):
|
||||
return self._import
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def addAuthor(self, value):
|
||||
"""Add an author to the authors list.
|
||||
"""
|
||||
self._authors.append(simplified(str(value)))
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def incSaveCount(self):
|
||||
"""Increment the save count by one.
|
||||
"""
|
||||
self._saveCount += 1
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def incAutoCount(self):
|
||||
"""Increment the auto save count by one.
|
||||
"""
|
||||
self._autoCount += 1
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
##
|
||||
# Getters
|
||||
##
|
||||
|
||||
def getLastHandle(self, component):
|
||||
"""Retrieve the last used handle for a given component.
|
||||
"""
|
||||
return self._lastHandle.get(component, None)
|
||||
|
||||
def getTitleFormat(self, kind):
|
||||
"""Retrieve the title format string for a given kind of header.
|
||||
"""
|
||||
return self._titleFormat.get(kind, "%title%")
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setUuid(self, value):
|
||||
"""Set the project id.
|
||||
"""
|
||||
value = checkUuid(value, "")
|
||||
if not value:
|
||||
self._uuid = str(uuid.uuid4())
|
||||
elif value != self._uuid:
|
||||
self._uuid = value
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setName(self, value):
|
||||
"""Set a new project name.
|
||||
"""
|
||||
if value != self._name:
|
||||
self._name = simplified(str(value))
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setTitle(self, value):
|
||||
"""Set a new novel title.
|
||||
"""
|
||||
if value != self._title:
|
||||
self._title = simplified(str(value))
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setAuthors(self, value):
|
||||
"""Set the list of authors from either a string with one author
|
||||
per line, or a list of authors.
|
||||
"""
|
||||
self._authors = []
|
||||
self.theProject.setProjectChanged(True)
|
||||
if isinstance(value, str):
|
||||
for author in value.splitlines():
|
||||
author = simplified(author)
|
||||
if author:
|
||||
self._authors.append(author)
|
||||
self.theProject.setProjectChanged(True)
|
||||
elif isinstance(value, list):
|
||||
self._authors = value
|
||||
return
|
||||
|
||||
def setSaveCount(self, value):
|
||||
"""Set the save count from last session.
|
||||
"""
|
||||
self._saveCount = checkInt(value, 0)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setAutoCount(self, value):
|
||||
"""Set the auto save count from last session.
|
||||
"""
|
||||
self._autoCount = checkInt(value, 0)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setEditTime(self, value):
|
||||
"""Set tyje edit time from last session.
|
||||
"""
|
||||
self._editTime = checkInt(value, 0)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setDoBackup(self, value):
|
||||
"""Set the do write backup flag.
|
||||
"""
|
||||
if value != self._doBackup:
|
||||
self._doBackup = checkBool(value, False)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setLanguage(self, value):
|
||||
"""Set the project language.
|
||||
"""
|
||||
if value != self._language:
|
||||
self._language = checkStringNone(value, None)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setSpellCheck(self, value):
|
||||
"""Set the spell check flag.
|
||||
"""
|
||||
if value != self._spellCheck:
|
||||
self._spellCheck = checkBool(value, False)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setSpellLang(self, value):
|
||||
"""Set the spell check language.
|
||||
"""
|
||||
if value != self._spellLang:
|
||||
self._spellLang = checkStringNone(value, None)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setLastHandle(self, value, component=None):
|
||||
"""Set a last used handle into the handle registry. If component
|
||||
is None, the value is assumed to be the whole dictionary of
|
||||
values.
|
||||
"""
|
||||
if isinstance(component, str):
|
||||
self._lastHandle[component] = checkStringNone(value, None)
|
||||
self.theProject.setProjectChanged(True)
|
||||
elif isinstance(value, dict):
|
||||
for key, entry in value.items():
|
||||
if key in self._lastHandle:
|
||||
self._lastHandle[key] = str(entry) if isHandle(entry) else None
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setInitCounts(self, novel=None, notes=None):
|
||||
"""Set the worc count totals for novel and note files.
|
||||
"""
|
||||
if novel is not None:
|
||||
self._initCounts[0] = checkInt(novel, 0)
|
||||
self._currCounts[0] = checkInt(novel, 0)
|
||||
if notes is not None:
|
||||
self._initCounts[1] = checkInt(notes, 0)
|
||||
self._currCounts[1] = checkInt(notes, 0)
|
||||
return
|
||||
|
||||
def setCurrCounts(self, novel=None, notes=None):
|
||||
"""Set the worc count totals for novel and note files.
|
||||
"""
|
||||
if novel is not None:
|
||||
self._currCounts[0] = checkInt(novel, 0)
|
||||
if notes is not None:
|
||||
self._currCounts[1] = checkInt(notes, 0)
|
||||
return
|
||||
|
||||
def setAutoReplace(self, value):
|
||||
"""Set the auto-replace dictionary.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
self._autoReplace = {}
|
||||
for key, entry in value.items():
|
||||
if isinstance(entry, str):
|
||||
self._autoReplace[key] = simplified(entry)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setTitleFormat(self, value):
|
||||
"""Set the title formats.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
for key, entry in value.items():
|
||||
if key in self._titleFormat and isinstance(entry, str):
|
||||
self._titleFormat[key] = simplified(entry)
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
# END Class NWProjectData
|
||||
@@ -0,0 +1,611 @@
|
||||
"""
|
||||
novelWriter – Project XML Read/Write
|
||||
====================================
|
||||
Classes for reading and writing the project XML file
|
||||
|
||||
File History:
|
||||
Created: 2022-09-28 [2.0rc1] ProjectXMLReader
|
||||
Created: 2022-09-28 [2.0rc1] XMLReadState
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from enum import Enum
|
||||
from lxml import etree
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.common import (
|
||||
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
||||
hexToInt, simplified, yesNo
|
||||
)
|
||||
from novelwriter.constants import nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FILE_VERSION = "1.5" # The current project file format version
|
||||
HEX_VERSION = 0x0105
|
||||
|
||||
NUM_VERSION = {
|
||||
"1.0": 0x0100, # Up to 0.7
|
||||
"1.1": 0x0101, # Up to 0.10
|
||||
"1.2": 0x0102, # Up to 1.5
|
||||
"1.3": 0x0103, # Up to 2.0 Beta 1
|
||||
"1.4": 0x0104, # Up to 2.0 RC 2
|
||||
"1.5": 0x0105, # Current
|
||||
}
|
||||
|
||||
|
||||
class XMLReadState(Enum):
|
||||
|
||||
NO_ACTION = 0
|
||||
NO_ERROR = 1
|
||||
PARSED_BACKUP = 2
|
||||
CANNOT_PARSE = 3
|
||||
NOT_NWX_FILE = 4
|
||||
UNKNOWN_VERSION = 5
|
||||
PARSED_OK = 6
|
||||
WAS_LEGACY = 7
|
||||
|
||||
# END Class XMLReadState
|
||||
|
||||
|
||||
class ProjectXMLReader:
|
||||
"""The main project XML file reader class. All data is read into a
|
||||
NWProjectData instance, which must be provided.
|
||||
|
||||
File Format Version Change History
|
||||
==================================
|
||||
1.0 Original file format.
|
||||
|
||||
1.1 Changes the way documents are structured in the project folder
|
||||
from data_X, where X is the first hex value of the handle, to a
|
||||
single content folder. Introduced in version 0.7.
|
||||
|
||||
1.2 Changes the way autoReplace entries are stored. Introduced in
|
||||
version 0.10.
|
||||
|
||||
1.3 Reduces the number of layouts to only two. One for novel
|
||||
documents and one for project notes. Introduced in version 1.5.
|
||||
|
||||
1.4 Introduces a more compact format for storing items. All settings
|
||||
aside from name are now attributes. This format also changes the
|
||||
way satus and importance labels are stored. This format was only
|
||||
a part of version 2.0 RC 1
|
||||
|
||||
1.5 The actual format released for 2.0. It moves last used handles
|
||||
and title formats into a key/value format similar to auto-
|
||||
replace, status and imporetance. It adds the heading value to
|
||||
the content item meta entry. It also moves meta data related to
|
||||
the project or the content into their respective section nodes
|
||||
as attributes. The id attribute was also added to the project.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
|
||||
self._path = Path(path)
|
||||
self._state = XMLReadState.NO_ACTION
|
||||
|
||||
self._root = ""
|
||||
self._version = 0x0
|
||||
self._appVersion = ""
|
||||
self._hexVersion = 0x0
|
||||
self._timeStamp = ""
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""The state of the parsing as an XMLReadState enum value.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def xmlRoot(self):
|
||||
"""The root tag name of the XNL file,
|
||||
"""
|
||||
return self._root
|
||||
|
||||
@property
|
||||
def xmlVersion(self):
|
||||
"""The project XML version number.
|
||||
"""
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def appVersion(self):
|
||||
"""The novelWriter version number who wrote the file.
|
||||
"""
|
||||
return self._appVersion
|
||||
|
||||
@property
|
||||
def hexVersion(self):
|
||||
"""The novelWriter version number who wrote the file as hex.
|
||||
"""
|
||||
return self._hexVersion
|
||||
|
||||
@property
|
||||
def timeStamp(self):
|
||||
"""The date and time when the file was written.
|
||||
"""
|
||||
return self._timeStamp
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def read(self, projData, projContent):
|
||||
"""Read and parse the project XML file.
|
||||
"""
|
||||
tStart = time()
|
||||
logger.debug("Reading project XML")
|
||||
|
||||
try:
|
||||
xml = etree.parse(str(self._path))
|
||||
self._state = XMLReadState.NO_ERROR
|
||||
|
||||
except Exception as exc:
|
||||
# Trying to open backup file instead
|
||||
logger.error("Failed to parse project XML", exc_info=exc)
|
||||
self._state = XMLReadState.CANNOT_PARSE
|
||||
|
||||
backFile = self._path.with_suffix(".bak")
|
||||
if backFile.is_file():
|
||||
try:
|
||||
xml = etree.parse(str(backFile))
|
||||
self._state = XMLReadState.PARSED_BACKUP
|
||||
logger.info("Backup project file parsed")
|
||||
except Exception as exc:
|
||||
logger.error("Failed to parse backup project XML", exc_info=exc)
|
||||
self._state = XMLReadState.CANNOT_PARSE
|
||||
return False
|
||||
else:
|
||||
self._state = XMLReadState.CANNOT_PARSE
|
||||
return False
|
||||
|
||||
xRoot = xml.getroot()
|
||||
self._root = str(xRoot.tag)
|
||||
if self._root != "novelWriterXML":
|
||||
self._state = XMLReadState.NOT_NWX_FILE
|
||||
return False
|
||||
|
||||
fileVersion = str(xRoot.attrib.get("fileVersion", ""))
|
||||
if fileVersion in NUM_VERSION:
|
||||
self._version = NUM_VERSION[fileVersion]
|
||||
else:
|
||||
self._state = XMLReadState.UNKNOWN_VERSION
|
||||
return False
|
||||
|
||||
logger.debug("XML is '%s' version '%s'", self._root, fileVersion)
|
||||
|
||||
self._appVersion = str(xRoot.attrib.get("appVersion", ""))
|
||||
self._hexVersion = hexToInt(xRoot.attrib.get("hexVersion", ""))
|
||||
self._timeStamp = str(xRoot.attrib.get("timeStamp", ""))
|
||||
|
||||
for xSection in xRoot:
|
||||
if xSection.tag == "project":
|
||||
self._parseProjectMeta(xSection, projData)
|
||||
elif xSection.tag == "settings":
|
||||
self._parseProjectSettings(xSection, projData)
|
||||
elif xSection.tag == "content":
|
||||
if self._version >= 0x0104:
|
||||
self._parseProjectContent(xSection, projData, projContent)
|
||||
else:
|
||||
self._parseProjectContentLegacy(xSection, projData, projContent)
|
||||
else:
|
||||
logger.warning("Ignored <root/%s> in XML", xSection.tag)
|
||||
|
||||
if self._version == HEX_VERSION:
|
||||
self._state = XMLReadState.PARSED_OK
|
||||
else:
|
||||
self._state = XMLReadState.WAS_LEGACY
|
||||
|
||||
logger.debug("Project XML loaded in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _parseProjectMeta(self, xSection, projData):
|
||||
"""Parse the project section of the XML file.
|
||||
"""
|
||||
logger.debug("Parsing <project> section")
|
||||
|
||||
projData.setUuid(xSection.attrib.get("id", None)) # Added in 1.5
|
||||
projData.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5
|
||||
projData.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5
|
||||
projData.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5
|
||||
|
||||
for xItem in xSection:
|
||||
if xItem.tag == "name":
|
||||
projData.setName(xItem.text)
|
||||
elif xItem.tag == "title":
|
||||
projData.setTitle(xItem.text)
|
||||
elif xItem.tag == "author":
|
||||
projData.addAuthor(xItem.text)
|
||||
else:
|
||||
logger.warning("Ignored <root/project/%s> in XML", xItem.tag)
|
||||
|
||||
# Deprecated Nodes
|
||||
if self._version < HEX_VERSION:
|
||||
for xItem in xSection:
|
||||
if xItem.tag == "saveCount": # Moved to attribute in 1.5
|
||||
projData.setSaveCount(xItem.text)
|
||||
elif xItem.tag == "autoCount": # Moved to attribute in 1.5
|
||||
projData.setAutoCount(xItem.text)
|
||||
elif xItem.tag == "editTime": # Moved to attribute in 1.5
|
||||
projData.setEditTime(xItem.text)
|
||||
|
||||
return
|
||||
|
||||
def _parseProjectSettings(self, xSection, projData):
|
||||
"""Parse the settings section of the XML file.
|
||||
"""
|
||||
logger.debug("Parsing <settings> section")
|
||||
|
||||
for xItem in xSection:
|
||||
if xItem.tag == "doBackup":
|
||||
projData.setDoBackup(xItem.text)
|
||||
elif xItem.tag == "language":
|
||||
projData.setLanguage(xItem.text)
|
||||
elif xItem.tag == "spellChecking":
|
||||
projData.setSpellLang(xItem.text)
|
||||
projData.setSpellCheck(xItem.attrib.get("auto", False))
|
||||
elif xItem.tag == "status":
|
||||
self._parseStatusImport(xItem, projData.itemStatus)
|
||||
elif xItem.tag == "importance":
|
||||
self._parseStatusImport(xItem, projData.itemImport)
|
||||
elif xItem.tag == "lastHandle":
|
||||
projData.setLastHandle(self._parseDictKeyText(xItem))
|
||||
elif xItem.tag == "autoReplace":
|
||||
if self._version >= 0x0102:
|
||||
projData.setAutoReplace(self._parseDictKeyText(xItem))
|
||||
else: # Pre 1.2 format
|
||||
projData.setAutoReplace(self._parseDictTagText(xItem))
|
||||
elif xItem.tag == "titleFormat":
|
||||
if self._version >= 0x0105:
|
||||
projData.setTitleFormat(self._parseDictKeyText(xItem))
|
||||
else: # Pre 1.4 format
|
||||
projData.setTitleFormat(self._parseDictTagText(xItem))
|
||||
else:
|
||||
logger.warning("Ignored <root/settings/%s> in XML", xItem.tag)
|
||||
|
||||
# Deprecated Nodes
|
||||
if self._version < HEX_VERSION:
|
||||
for xItem in xSection:
|
||||
if xItem.tag == "spellCheck": # Changed to spellChecking in 1.5
|
||||
projData.setSpellCheck(xItem.text)
|
||||
elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
|
||||
projData.setSpellLang(xItem.text)
|
||||
elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
|
||||
projData.setInitCounts(novel=xItem.text)
|
||||
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
|
||||
projData.setInitCounts(notes=xItem.text)
|
||||
|
||||
return
|
||||
|
||||
def _parseProjectContent(self, xSection, projData, projContent):
|
||||
"""Parse the content section of the XML file.
|
||||
"""
|
||||
logger.debug("Parsing <content> section")
|
||||
|
||||
projData.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
|
||||
projData.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
|
||||
|
||||
for xItem in xSection:
|
||||
if xItem.tag != "item":
|
||||
logger.warning("Ignored item <root/content/%s> in XML", xItem.tag)
|
||||
continue
|
||||
|
||||
item = {}
|
||||
meta = {}
|
||||
name = {}
|
||||
itemName = ""
|
||||
|
||||
item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
|
||||
item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
|
||||
item["root"] = checkStringNone(xItem.attrib.get("root"), None)
|
||||
item["order"] = checkInt(xItem.attrib.get("order"), 0)
|
||||
item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE")
|
||||
item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS")
|
||||
item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
|
||||
for xVal in xItem:
|
||||
if xVal.tag == "meta":
|
||||
meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
|
||||
meta["heading"] = checkString(xVal.attrib.get("heading"), "H0")
|
||||
meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
|
||||
meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
|
||||
meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
|
||||
meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
|
||||
elif xVal.tag == "name":
|
||||
itemName = simplified(checkString(xVal.text, ""))
|
||||
name["status"] = checkStringNone(xVal.attrib.get("status"), None)
|
||||
name["import"] = checkStringNone(xVal.attrib.get("import"), None)
|
||||
name["active"] = checkBool(xVal.attrib.get("active"), False)
|
||||
else:
|
||||
logger.warning("Ignored <root/content/item/%s> in XML", xVal.tag)
|
||||
|
||||
# Deprecated Nodes
|
||||
if self._version < HEX_VERSION:
|
||||
for xVal in xItem:
|
||||
if xVal.tag == "name" and "exported" in xVal.attrib:
|
||||
name["active"] = checkBool(xVal.attrib.get("exported"), False)
|
||||
|
||||
projContent.append({
|
||||
"name": itemName,
|
||||
"itemAttr": item,
|
||||
"metaAttr": meta,
|
||||
"nameAttr": name,
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
def _parseProjectContentLegacy(self, xSection, projData, projContent):
|
||||
"""Parse the content section of the XML file for older versions.
|
||||
"""
|
||||
logger.debug("Parsing <content> section (legacy format)")
|
||||
|
||||
# Create maps to look up name -> key for status and importance
|
||||
statusMap = {entry.get("name"): key for key, entry in projData.itemStatus.items()}
|
||||
importMap = {entry.get("name"): key for key, entry in projData.itemImport.items()}
|
||||
|
||||
for xItem in xSection:
|
||||
if xItem.tag != "item":
|
||||
logger.warning("Ignored item <root/content/%s> in XML", xItem.tag)
|
||||
continue
|
||||
|
||||
item = {}
|
||||
meta = {}
|
||||
name = {}
|
||||
itemName = ""
|
||||
|
||||
item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
|
||||
item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
|
||||
item["root"] = None # Value was added in 1.4
|
||||
item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
|
||||
meta["heading"] = "H0" # Value was added in 1.4
|
||||
|
||||
tmpStatus = ""
|
||||
for xVal in xItem:
|
||||
if xVal.tag == "name":
|
||||
itemName = simplified(checkString(xVal.text, ""))
|
||||
elif xVal.tag == "status":
|
||||
tmpStatus = checkStringNone(xVal.text, None)
|
||||
elif xVal.tag == "type":
|
||||
item["type"] = checkString(xVal.text, "")
|
||||
elif xVal.tag == "class":
|
||||
item["class"] = checkString(xVal.text, "")
|
||||
elif xVal.tag == "layout":
|
||||
item["layout"] = checkString(xVal.text, "")
|
||||
elif xVal.tag == "expanded":
|
||||
meta["expanded"] = checkBool(xVal.text, False)
|
||||
elif xVal.tag == "exported": # Renamed to active in 1.5
|
||||
name["active"] = checkBool(xVal.text, False)
|
||||
elif xVal.tag == "charCount":
|
||||
meta["charCount"] = checkInt(xVal.text, 0)
|
||||
elif xVal.tag == "wordCount":
|
||||
meta["wordCount"] = checkInt(xVal.text, 0)
|
||||
elif xVal.tag == "paraCount":
|
||||
meta["paraCount"] = checkInt(xVal.text, 0)
|
||||
elif xVal.tag == "cursorPos":
|
||||
meta["cursorPos"] = checkInt(xVal.text, 0)
|
||||
else:
|
||||
logger.warning("Ignored <root/content/item/%s> in XML", xVal.tag)
|
||||
|
||||
# Status was split into separate status/import with a key in 1.4
|
||||
if item.get("class", "") in ("NOVEL", "ARCHIVE"):
|
||||
name["status"] = statusMap.get(tmpStatus, None)
|
||||
else:
|
||||
name["import"] = importMap.get(tmpStatus, None)
|
||||
|
||||
# A number of layouts were removed in 1.3
|
||||
if item.get("layout", "") in (
|
||||
"TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"
|
||||
):
|
||||
item["layout"] = "DOCUMENT"
|
||||
|
||||
# The trash type was removed in 1.4
|
||||
if item.get("type", "") == "TRASH":
|
||||
item["type"] = "ROOT"
|
||||
|
||||
projContent.append({
|
||||
"name": itemName,
|
||||
"itemAttr": item,
|
||||
"metaAttr": meta,
|
||||
"nameAttr": name,
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
def _parseStatusImport(self, xItem, sObject):
|
||||
"""Parse a status or importance entry.
|
||||
"""
|
||||
for xEntry in xItem:
|
||||
if xEntry.tag == "entry":
|
||||
key = xEntry.attrib.get("key", None)
|
||||
red = checkInt(xEntry.attrib.get("red", 0), 0)
|
||||
green = checkInt(xEntry.attrib.get("green", 0), 0)
|
||||
blue = checkInt(xEntry.attrib.get("blue", 0), 0)
|
||||
count = checkInt(xEntry.attrib.get("count", 0), 0)
|
||||
sObject.write(key, xEntry.text, (red, green, blue), count)
|
||||
return
|
||||
|
||||
def _parseDictKeyText(self, xItem):
|
||||
"""Parse a dictionary stored with key as an attribute and the
|
||||
value as the text porperty.
|
||||
"""
|
||||
result = {}
|
||||
for xEntry in xItem:
|
||||
if xEntry.tag == "entry" and "key" in xEntry.attrib:
|
||||
result[xEntry.attrib["key"]] = checkString(xEntry.text, "")
|
||||
return result
|
||||
|
||||
def _parseDictTagText(self, xItem):
|
||||
"""Parse a dictionary stored with key as the tag and the value
|
||||
as the text porperty.
|
||||
"""
|
||||
return {xNode.tag: checkString(xNode.text, "") for xNode in xItem}
|
||||
|
||||
# END Class ProjectXMLReader
|
||||
|
||||
|
||||
class ProjectXMLWriter:
|
||||
|
||||
def __init__(self, path):
|
||||
|
||||
self._path = Path(path)
|
||||
self._error = None
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._error
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def write(self, projData, projContent, saveTime, editTime):
|
||||
"""Write the project data and content to the XML files.
|
||||
"""
|
||||
tStart = time()
|
||||
logger.debug("Writing project XML")
|
||||
|
||||
xRoot = etree.Element("novelWriterXML", attrib={
|
||||
"appVersion": str(novelwriter.__version__),
|
||||
"hexVersion": str(novelwriter.__hexversion__),
|
||||
"fileVersion": FILE_VERSION,
|
||||
"timeStamp": formatTimeStamp(saveTime),
|
||||
})
|
||||
|
||||
# Save Project Meta
|
||||
projAttr = {
|
||||
"id": projData.uuid,
|
||||
"saveCount": str(projData.saveCount),
|
||||
"autoCount": str(projData.autoCount),
|
||||
"editTime": str(editTime),
|
||||
}
|
||||
|
||||
xProject = etree.SubElement(xRoot, "project", attrib=projAttr)
|
||||
self._packSingleValue(xProject, "name", projData.name)
|
||||
self._packSingleValue(xProject, "title", projData.title)
|
||||
self._packListValue(xProject, "author", projData.authors)
|
||||
|
||||
# Save Project Settings
|
||||
xSettings = etree.SubElement(xRoot, "settings")
|
||||
self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
|
||||
self._packSingleValue(xSettings, "language", projData.language)
|
||||
self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
|
||||
"auto": yesNo(projData.spellCheck)
|
||||
})
|
||||
self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle)
|
||||
self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
|
||||
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
|
||||
|
||||
# Save Status/Importance
|
||||
xStatus = etree.SubElement(xSettings, "status")
|
||||
for label, attrib in projData.itemStatus.pack():
|
||||
self._packSingleValue(xStatus, "entry", label, attrib=attrib)
|
||||
|
||||
xImport = etree.SubElement(xSettings, "importance")
|
||||
for label, attrib in projData.itemImport.pack():
|
||||
self._packSingleValue(xImport, "entry", label, attrib=attrib)
|
||||
|
||||
# Save Tree Content
|
||||
contAttr = {
|
||||
"items": str(len(projContent)),
|
||||
"novelWords": str(projData.currCounts[0]),
|
||||
"notesWords": str(projData.currCounts[1]),
|
||||
}
|
||||
|
||||
xContent = etree.SubElement(xRoot, "content", attrib=contAttr)
|
||||
for item in projContent:
|
||||
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
|
||||
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
|
||||
xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
|
||||
xName.text = item["name"]
|
||||
|
||||
# Write the XML tree to file
|
||||
saveFile = self._path / nwFiles.PROJ_FILE
|
||||
tempFile = saveFile.with_suffix(".tmp")
|
||||
backFile = saveFile.with_suffix(".bak")
|
||||
try:
|
||||
tempFile.write_bytes(etree.tostring(
|
||||
xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
|
||||
))
|
||||
except Exception as exc:
|
||||
self._error = exc
|
||||
return False
|
||||
|
||||
# If we're here, the file was successfully saved,
|
||||
# so let's sort out the temps and backups
|
||||
try:
|
||||
if saveFile.exists():
|
||||
saveFile.replace(backFile)
|
||||
tempFile.replace(saveFile)
|
||||
except Exception as exc:
|
||||
self._error = exc
|
||||
return False
|
||||
|
||||
logger.debug("Project XML saved in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _packSingleValue(self, xParent, name, value, attrib=None):
|
||||
"""Pack a single value into an XML element.
|
||||
"""
|
||||
xItem = etree.SubElement(xParent, name, attrib=attrib)
|
||||
xItem.text = str(value) or ""
|
||||
return
|
||||
|
||||
def _packListValue(self, xParent, name, data):
|
||||
"""Pack a list of values into an XML element.
|
||||
"""
|
||||
for value in data:
|
||||
xItem = etree.SubElement(xParent, name)
|
||||
xItem.text = str(value) or ""
|
||||
return
|
||||
|
||||
def _packDictKeyValue(self, xParent, name, data):
|
||||
"""Pack the entries of a dictionary into an XML element.
|
||||
"""
|
||||
xItem = etree.SubElement(xParent, name)
|
||||
for key, value in data.items():
|
||||
if len(key) > 0:
|
||||
xEntry = etree.SubElement(xItem, "entry", attrib={"key": key})
|
||||
xEntry.text = str(value) or ""
|
||||
return
|
||||
|
||||
# END Class ProjectXMLWriter
|
||||
@@ -23,15 +23,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.error import logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWSpellEnchant():
|
||||
class NWSpellEnchant:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -46,36 +48,47 @@ class NWSpellEnchant():
|
||||
return
|
||||
|
||||
##
|
||||
# Getters and Setters
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def spellLanguage(self):
|
||||
return self._spellLanguage
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setLanguage(self, theLang, projectDict=None):
|
||||
"""Load a dictionary for the language specified in the config.
|
||||
If that fails, we load a mock dictionary so that lookups don't
|
||||
crash.
|
||||
crash. Note that enchant will allow loading an empty string as
|
||||
a tag, but this will fail later on. See issue #1096.
|
||||
"""
|
||||
self._theBroker = None
|
||||
self._theDict = None
|
||||
self._spellLanguage = None
|
||||
|
||||
try:
|
||||
import enchant
|
||||
if self._theBroker is not None:
|
||||
logger.debug("Deleting old pyenchant broker")
|
||||
del self._theBroker
|
||||
|
||||
self._theBroker = enchant.Broker()
|
||||
self._theDict = self._theBroker.request_dict(theLang)
|
||||
self._spellLanguage = theLang
|
||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||
if theLang and enchant.dict_exists(theLang):
|
||||
self._theBroker = enchant.Broker()
|
||||
self._theDict = self._theBroker.request_dict(theLang)
|
||||
self._spellLanguage = theLang
|
||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||
else:
|
||||
logger.warning("Enchant found no dictionary for language '%s'", theLang)
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
||||
self._theDict = FakeEnchant()
|
||||
self._spellLanguage = None
|
||||
|
||||
self._readProjectDictionary(projectDict)
|
||||
for pWord in self._projDict:
|
||||
self._theDict.add_to_session(pWord)
|
||||
if self._theDict is None:
|
||||
self._theDict = FakeEnchant()
|
||||
else:
|
||||
self._readProjectDictionary(projectDict)
|
||||
for pWord in self._projDict:
|
||||
self._theDict.add_to_session(pWord)
|
||||
|
||||
return
|
||||
|
||||
@@ -160,10 +173,10 @@ class NWSpellEnchant():
|
||||
self._projDict = set()
|
||||
self._projectDict = projectDict
|
||||
|
||||
if projectDict is None:
|
||||
if not isinstance(projectDict, Path):
|
||||
return False
|
||||
|
||||
if not os.path.isfile(projectDict):
|
||||
if not projectDict.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -189,6 +202,9 @@ class FakeEnchant:
|
||||
"""Fallback for when Enchant is selected, but not installed.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.tag = ""
|
||||
self.provider = namedtuple("provider", "name")
|
||||
self.provider.name = ""
|
||||
return
|
||||
|
||||
def check(self, theWord):
|
||||
|
||||
@@ -28,16 +28,15 @@ import random
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from lxml import etree
|
||||
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
||||
from PyQt5.QtCore import QRectF, Qt
|
||||
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor
|
||||
|
||||
from novelwriter.common import checkInt, minmax, simplified
|
||||
from novelwriter.common import minmax, simplified
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWStatus():
|
||||
class NWStatus:
|
||||
|
||||
STATUS = 1
|
||||
IMPORT = 2
|
||||
@@ -46,13 +45,17 @@ class NWStatus():
|
||||
|
||||
self._type = type
|
||||
self._store = {}
|
||||
self._reverse = {}
|
||||
self._default = None
|
||||
|
||||
self._iconSize = novelwriter.CONFIG.pxInt(32)
|
||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
||||
pixmap.fill(QColor(100, 100, 100))
|
||||
self._defaultIcon = QIcon(pixmap)
|
||||
self._iPX = novelwriter.CONFIG.pxInt(24)
|
||||
|
||||
pA = novelwriter.CONFIG.pxInt(2)
|
||||
pB = novelwriter.CONFIG.pxInt(20)
|
||||
pR = float(novelwriter.CONFIG.pxInt(4))
|
||||
self._iconPath = QPainterPath()
|
||||
self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR)
|
||||
|
||||
self._defaultIcon = self._createIcon(100, 100, 100)
|
||||
|
||||
if self._type == self.STATUS:
|
||||
self._prefix = "s"
|
||||
@@ -63,31 +66,30 @@ class NWStatus():
|
||||
|
||||
return
|
||||
|
||||
def write(self, key, name, cols, count=None):
|
||||
def write(self, key, name, col, count=None):
|
||||
"""Add or update a status entry. If the key is invalid, a new
|
||||
key is generated.
|
||||
"""
|
||||
if not self._isKey(key):
|
||||
key = self._newKey()
|
||||
if not isinstance(cols, tuple):
|
||||
cols = (100, 100, 100)
|
||||
if len(cols) != 3:
|
||||
cols = (100, 100, 100)
|
||||
|
||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
||||
pixmap.fill(QColor(*cols))
|
||||
if not isinstance(col, tuple):
|
||||
col = (100, 100, 100)
|
||||
if len(col) != 3:
|
||||
col = (100, 100, 100)
|
||||
|
||||
cR = minmax(col[0], 0, 255)
|
||||
cG = minmax(col[1], 0, 255)
|
||||
cB = minmax(col[2], 0, 255)
|
||||
name = simplified(name)
|
||||
if count is None:
|
||||
count = self._store[key]["count"] if key in self._store else 0
|
||||
count = self._store.get(key, {}).get("count", 0)
|
||||
|
||||
self._store[key] = {
|
||||
"name": name,
|
||||
"icon": QIcon(pixmap),
|
||||
"cols": cols,
|
||||
"icon": self._createIcon(cR, cG, cB),
|
||||
"cols": (cR, cG, cB),
|
||||
"count": count,
|
||||
}
|
||||
self._reverse[name] = key
|
||||
|
||||
if self._default is None:
|
||||
self._default = key
|
||||
@@ -103,7 +105,6 @@ class NWStatus():
|
||||
if self._store[key]["count"] > 0:
|
||||
return False
|
||||
|
||||
del self._reverse[self._store[key]["name"]]
|
||||
del self._store[key]
|
||||
|
||||
keys = list(self._store.keys())
|
||||
@@ -120,8 +121,6 @@ class NWStatus():
|
||||
"""
|
||||
if self._isKey(value) and value in self._store:
|
||||
return value
|
||||
elif value in self._reverse:
|
||||
return self._reverse[value]
|
||||
elif self._default is not None:
|
||||
return self._default
|
||||
else:
|
||||
@@ -203,37 +202,30 @@ class NWStatus():
|
||||
self._store[key]["count"] += 1
|
||||
return
|
||||
|
||||
def packXML(self, xParent):
|
||||
"""Pack the status entries into an XML object for saving to the
|
||||
main project file.
|
||||
def pack(self):
|
||||
"""Pack the status entries into a dictionary.
|
||||
"""
|
||||
for key, data in self._store.items():
|
||||
xSub = etree.SubElement(xParent, "entry", attrib={
|
||||
yield (data["name"], {
|
||||
"key": key,
|
||||
"count": str(data["count"]),
|
||||
"red": str(data["cols"][0]),
|
||||
"green": str(data["cols"][1]),
|
||||
"blue": str(data["cols"][2]),
|
||||
})
|
||||
xSub.text = data["name"]
|
||||
return
|
||||
|
||||
return True
|
||||
|
||||
def unpackXML(self, xParent):
|
||||
"""Unpack an XML tree and set the class values.
|
||||
def unpack(self, data):
|
||||
"""Unpack a data dictionary and set the class values.
|
||||
"""
|
||||
self._store = {}
|
||||
self._reverse = {}
|
||||
self._default = None
|
||||
|
||||
for xChild in xParent:
|
||||
key = xChild.attrib.get("key", None)
|
||||
name = xChild.text.strip()
|
||||
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
|
||||
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
|
||||
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
|
||||
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
|
||||
self.write(key, name, (red, green, blue), count)
|
||||
for key, entry in data.items():
|
||||
label = entry.get("label", "")
|
||||
colour = entry.get("colour", (100, 100, 100))
|
||||
count = entry.get("count", 0)
|
||||
self.write(key, label, colour, count)
|
||||
|
||||
return True
|
||||
|
||||
@@ -267,6 +259,19 @@ class NWStatus():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _createIcon(self, red, green, blue):
|
||||
"""Generate an icon for a status label.
|
||||
"""
|
||||
pixmap = QPixmap(self._iPX, self._iPX)
|
||||
pixmap.fill(Qt.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.fillPath(self._iconPath, QColor(red, green, blue))
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
##
|
||||
# Iterator Bits
|
||||
##
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
"""
|
||||
novelWriter – Project Storage Class
|
||||
===================================
|
||||
The main class handling the project storage
|
||||
|
||||
File History:
|
||||
Created: 2022-11-01 [2.0rc1] NWStorage
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
||||
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.document import NWDocument
|
||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
||||
from novelwriter.error import logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWStorage:
|
||||
|
||||
MODE_INACTIVE = 0
|
||||
MODE_INPLACE = 1
|
||||
MODE_ARCHIVE = 2
|
||||
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theProject = theProject
|
||||
|
||||
self._storagePath = None
|
||||
self._runtimePath = None
|
||||
self._lockFilePath = None
|
||||
self._openMode = self.MODE_INACTIVE
|
||||
|
||||
return
|
||||
|
||||
def clear(self):
|
||||
"""Reset internal variables.
|
||||
"""
|
||||
self._storagePath = None
|
||||
self._runtimePath = None
|
||||
self._openMode = self.MODE_INACTIVE
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def storagePath(self):
|
||||
return self._storagePath
|
||||
|
||||
@property
|
||||
def runtimePath(self):
|
||||
return self._runtimePath
|
||||
|
||||
@property
|
||||
def contentPath(self):
|
||||
if self._runtimePath is not None:
|
||||
return self._runtimePath / "content"
|
||||
return None
|
||||
|
||||
##
|
||||
# Core Methods
|
||||
##
|
||||
|
||||
def isOpen(self):
|
||||
"""Check if the storage location is open.
|
||||
"""
|
||||
return self._runtimePath is not None
|
||||
|
||||
def openProjectInPlace(self, path, newProject=False):
|
||||
"""Open a novelWriter project in-place. That is, it is opened
|
||||
directly from a project folder.
|
||||
"""
|
||||
inPath = Path(path).resolve()
|
||||
if inPath.is_file():
|
||||
# The path should not point to an exisitng file,
|
||||
# but it can point to a folder containing files
|
||||
inPath = inPath.parent
|
||||
|
||||
self._storagePath = inPath
|
||||
self._runtimePath = inPath
|
||||
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
|
||||
self._openMode = self.MODE_INPLACE
|
||||
|
||||
if not self._prepareStorage(checkLegacy=True, newProject=newProject):
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def openProjectArchive(self, path): # pragma: no cover
|
||||
pass
|
||||
|
||||
def runPostSaveTasks(self, autoSave=False): # pragma: no cover
|
||||
"""Run tasks after the project has been saved.
|
||||
"""
|
||||
if self._openMode == self.MODE_INPLACE:
|
||||
# Nothing to do, so we just return
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def closeSession(self):
|
||||
"""Run tasks related to closing the session.
|
||||
"""
|
||||
# Clear lockfile
|
||||
self.clear()
|
||||
return
|
||||
|
||||
##
|
||||
# Content Access Methods
|
||||
##
|
||||
|
||||
def getXmlReader(self):
|
||||
"""Return a properly configured ProjectXMLReader instance.
|
||||
"""
|
||||
if self._runtimePath is None:
|
||||
return None
|
||||
|
||||
projFile = self._runtimePath / nwFiles.PROJ_FILE
|
||||
xmlReader = ProjectXMLReader(projFile)
|
||||
|
||||
return xmlReader
|
||||
|
||||
def getXmlWriter(self):
|
||||
"""Return a properly configured ProjectXMLWriter instance.
|
||||
"""
|
||||
if self._runtimePath is None:
|
||||
return None
|
||||
|
||||
xmlWriter = ProjectXMLWriter(self._runtimePath)
|
||||
|
||||
return xmlWriter
|
||||
|
||||
def getDocument(self, tHandle):
|
||||
"""Return a document wrapper object.
|
||||
"""
|
||||
if self._runtimePath is not None:
|
||||
return NWDocument(self.theProject, tHandle)
|
||||
return NWDocument(self.theProject, None)
|
||||
|
||||
def getMetaFile(self, fileName):
|
||||
"""Return the path to a file in the project meta folder.
|
||||
"""
|
||||
if self._runtimePath is not None:
|
||||
return self._runtimePath / "meta" / fileName
|
||||
return None
|
||||
|
||||
def getCacheFile(self, fileName):
|
||||
"""Return the path to a file in the project cache folder.
|
||||
"""
|
||||
if self._runtimePath is not None:
|
||||
return self._runtimePath / "cache" / fileName
|
||||
return None
|
||||
|
||||
def readLockFile(self):
|
||||
"""Read the project lock file.
|
||||
"""
|
||||
if self._lockFilePath is None:
|
||||
return ["ERROR"]
|
||||
|
||||
if not self._lockFilePath.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
lines = self._lockFilePath.read_text(encoding="utf-8").split(";")
|
||||
except Exception:
|
||||
logger.error("Failed to read project lockfile")
|
||||
logException()
|
||||
return ["ERROR"]
|
||||
|
||||
if len(lines) != 4:
|
||||
return ["ERROR"]
|
||||
|
||||
return lines
|
||||
|
||||
def writeLockFile(self):
|
||||
"""Write the project lock file.
|
||||
"""
|
||||
if self._lockFilePath is None:
|
||||
return False
|
||||
|
||||
data = [
|
||||
self.mainConf.hostName, self.mainConf.osType,
|
||||
self.mainConf.kernelVer, str(int(time()))
|
||||
]
|
||||
try:
|
||||
self._lockFilePath.write_text(";".join(data), encoding="utf-8")
|
||||
except Exception:
|
||||
logger.error("Failed to write project lockfile")
|
||||
logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def clearLockFile(self):
|
||||
"""Remove the lock file, if it exists.
|
||||
"""
|
||||
if self._lockFilePath is None:
|
||||
return False
|
||||
|
||||
if self._lockFilePath.exists():
|
||||
try:
|
||||
self._lockFilePath.unlink()
|
||||
except Exception:
|
||||
logger.error("Failed to remove project lockfile")
|
||||
logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def zipIt(self, target, compression=None):
|
||||
"""Zip the content of the project at its runtime location into a
|
||||
zip file. This process will only grab files that are supposed to
|
||||
be in the project. All non-project files will be left out.
|
||||
"""
|
||||
basePath = self._runtimePath
|
||||
if not isinstance(basePath, Path):
|
||||
logger.error("No path set")
|
||||
return False
|
||||
|
||||
baseMeta = basePath / "meta"
|
||||
baseCont = basePath / "content"
|
||||
files = [
|
||||
(basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
|
||||
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
|
||||
(baseMeta / nwFiles.SESS_STATS, f"meta/{nwFiles.SESS_STATS}"),
|
||||
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
|
||||
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
|
||||
]
|
||||
for contItem in baseCont.iterdir():
|
||||
name = contItem.name
|
||||
if contItem.is_file() and len(name) == 17 and name.endswith(".nwd"):
|
||||
files.append((contItem, f"content/{name}"))
|
||||
|
||||
comp = ZIP_STORED if compression is None else ZIP_DEFLATED
|
||||
level = minmax(compression, 0, 9) if isinstance(compression, int) else None
|
||||
try:
|
||||
with ZipFile(target, mode="w", compression=comp, compresslevel=level) as zipObj:
|
||||
logger.info("Creating archive: %s", target)
|
||||
for srcPath, zipPath in files:
|
||||
if srcPath.is_file():
|
||||
zipObj.write(srcPath, zipPath)
|
||||
logger.debug("Added: %s", zipPath)
|
||||
except Exception:
|
||||
logger.error("Failed to create acrhive")
|
||||
logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _prepareStorage(self, checkLegacy=True, newProject=False):
|
||||
"""Prepare the storage area for the project.
|
||||
"""
|
||||
path = self._runtimePath
|
||||
if not isinstance(path, Path):
|
||||
logger.error("No path set")
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
if path == Path.home().absolute():
|
||||
logger.error("Cannot use the user's home path as the root of a project")
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
if newProject:
|
||||
# If it's a new project, we check that there is no existing
|
||||
# project in the selected path.
|
||||
if path.exists() and len(list(path.iterdir())) > 0:
|
||||
logger.error("The new project folder is not empty")
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
# The folder is not required to exist, as it could be a new
|
||||
# project, so we make sure it does. Then we add subfolders.
|
||||
try:
|
||||
path.mkdir(exist_ok=True)
|
||||
(path / "content").mkdir(exist_ok=True)
|
||||
(path / "cache").mkdir(exist_ok=True)
|
||||
(path / "meta").mkdir(exist_ok=True)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to create required project folders", exc_info=exc)
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
if not checkLegacy:
|
||||
# The legacy content check is only needed for project folder
|
||||
# storage, so if it is not expected to be that, there's no
|
||||
# need for the remaning checks.
|
||||
return True
|
||||
|
||||
# Check for legacy data folders
|
||||
for child in path.iterdir():
|
||||
if child.is_dir() and child.name.startswith("data_"):
|
||||
self._legacyDataFolder(path, child)
|
||||
|
||||
# Check for no longer used files, and delete them
|
||||
self._deleteDeprecatedFiles(path)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Legacy Project Data Handlers
|
||||
##
|
||||
|
||||
def _legacyDataFolder(self, path: Path, child: Path):
|
||||
"""Handle the content of a legacy data folder from a version 1.0
|
||||
project.
|
||||
"""
|
||||
logger.info("Processing legacy data folder: %s", path)
|
||||
|
||||
# Move Documents to Content
|
||||
first = child.name[-1]
|
||||
if first not in "0123456789abcdef":
|
||||
return
|
||||
|
||||
for item in child.iterdir():
|
||||
if not item.is_file():
|
||||
continue
|
||||
|
||||
name = item.name
|
||||
if len(name) == 21 and name.endswith("_main.nwd"):
|
||||
newPath = path / "content" / f"{first}{name[:12]}.nwd"
|
||||
try:
|
||||
item.rename(newPath)
|
||||
logger.info("Moved file: %s", newPath)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to move: %s", item, exc_info=exc)
|
||||
elif len(name) == 21 and name.endswith("_main.bak"):
|
||||
try:
|
||||
item.unlink()
|
||||
logger.info("Deleted file: %s", item)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to delete: %s", item, exc_info=exc)
|
||||
|
||||
# Remove Data Folder
|
||||
try:
|
||||
child.rmdir()
|
||||
logger.info("Deleted folder: %s", child)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to delete: %s", child, exc_info=exc)
|
||||
|
||||
return
|
||||
|
||||
def _deleteDeprecatedFiles(self, path: Path):
|
||||
"""Delete files that are no longer used by novelWriter.
|
||||
"""
|
||||
remove = [
|
||||
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
||||
path / "meta" / "exportOptions.json", # Replaced in 0.5
|
||||
path / "meta" / "outlineOptions.json", # Replaced in 0.5
|
||||
path / "meta" / "timelineOptions.json", # Replaced in 0.5
|
||||
path / "meta" / "docMergeOptions.json", # Replaced in 0.5
|
||||
path / "meta" / "sessionLogOptions.json", # Replaced in 0.5
|
||||
path / "ToC.json", # Dropped in 1.0 RC 1
|
||||
]
|
||||
for item in remove:
|
||||
if item.is_file():
|
||||
try:
|
||||
item.unlink()
|
||||
logger.info("Deleted: %s", item)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to delete: %s", item, exc_info=exc)
|
||||
|
||||
return
|
||||
|
||||
# END Class NWStorage
|
||||
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
|
||||
M_EBOOK = 2 # Tweak output for converting to epub
|
||||
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._genMode = self.M_EXPORT
|
||||
self._cssStyles = True
|
||||
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
|
||||
"""Extend the auto-replace to also properly encode some unicode
|
||||
characters into their respective HTML entities.
|
||||
"""
|
||||
Tokenizer.doPreProcessing(self)
|
||||
super().doPreProcessing()
|
||||
self._theText = self._theText.translate(self._trMap)
|
||||
return
|
||||
|
||||
@@ -315,7 +315,7 @@ class ToHtml(Tokenizer):
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
).format(
|
||||
projTitle=self.theProject.projName,
|
||||
projTitle=self.theProject.data.name,
|
||||
htmlStyle="\n".join(theStyle),
|
||||
bodyText=bodyText,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,6 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
from novelwriter.enum import nwItemLayout, nwItemType
|
||||
from novelwriter.common import numberToRoman, checkInt
|
||||
from novelwriter.constants import nwConst, nwRegEx, nwUnicode
|
||||
from novelwriter.core.document import NWDoc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -304,16 +303,10 @@ class Tokenizer(ABC):
|
||||
if self._theItem is None:
|
||||
return False
|
||||
|
||||
self._theText = ""
|
||||
if theText is not None:
|
||||
# If the text is set, just use that
|
||||
self._theText = theText
|
||||
else:
|
||||
# Otherwise, load it from file
|
||||
theDoc = NWDoc(self.theProject, theHandle)
|
||||
theText = theDoc.readDocument()
|
||||
if theText:
|
||||
self._theText = theText
|
||||
if theText is None:
|
||||
theText = self.theProject.storage.getDocument(theHandle).readDocument() or ""
|
||||
|
||||
self._theText = theText
|
||||
|
||||
docSize = len(self._theText)
|
||||
if docSize > nwConst.MAX_DOCSIZE:
|
||||
@@ -333,9 +326,10 @@ class Tokenizer(ABC):
|
||||
"""Run trough the various replace doctionaries.
|
||||
"""
|
||||
# Process the user's auto-replace dictionary
|
||||
if len(self.theProject.autoReplace) > 0:
|
||||
autoReplace = self.theProject.data.autoReplace
|
||||
if len(autoReplace) > 0:
|
||||
repDict = {}
|
||||
for aKey, aVal in self.theProject.autoReplace.items():
|
||||
for aKey, aVal in autoReplace.items():
|
||||
repDict[f"<{aKey}>"] = aVal
|
||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||
self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText)
|
||||
|
||||
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
|
||||
M_GH = 1 # GitHub Markdown
|
||||
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._genMode = self.M_STD
|
||||
self._fullMD = []
|
||||
|
||||
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
|
||||
class ToOdt(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, isFlat):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||||
|
||||
@@ -261,8 +261,8 @@ class ToOdt(Tokenizer):
|
||||
# ===============
|
||||
|
||||
if self._headerText == "":
|
||||
theTitle = self.theProject.bookTitle
|
||||
theAuth = self.theProject.getAuthors()
|
||||
theTitle = self.theProject.data.title
|
||||
theAuth = self.theProject.getFormattedAuthors()
|
||||
self._headerText = f"{theTitle} / {theAuth} /"
|
||||
|
||||
# Create Roots
|
||||
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
|
||||
# Auto-Style Classes
|
||||
# =============================================================================================== #
|
||||
|
||||
class ODTParagraphStyle():
|
||||
class ODTParagraphStyle:
|
||||
"""Wrapper class for the paragraph style setting used by the
|
||||
exporter. Only the used settings are exposed here to keep the class
|
||||
minimal and fast.
|
||||
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
|
||||
# END Class ODTParagraphStyle
|
||||
|
||||
|
||||
class ODTTextStyle():
|
||||
class ODTTextStyle:
|
||||
"""Wrapper class for the text style setting used by the exporter.
|
||||
Only the used settings are exposed here to keep the class minimal
|
||||
and fast.
|
||||
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
|
||||
X_SPAN_SING = 3
|
||||
|
||||
|
||||
class XMLParagraph():
|
||||
class XMLParagraph:
|
||||
"""This is a helper class to manage the text content of a single
|
||||
XML element using mixed content tags.
|
||||
|
||||
|
||||
@@ -23,13 +23,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkHandle
|
||||
from novelwriter.constants import nwFiles
|
||||
@@ -38,7 +37,7 @@ from novelwriter.core.item import NWItem
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWTree():
|
||||
class NWTree:
|
||||
|
||||
MAX_DEPTH = 1000 # Cap of tree traversing for loops
|
||||
|
||||
@@ -89,20 +88,20 @@ class NWTree():
|
||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
|
||||
nwItem.setHandle(tHandle)
|
||||
nwItem.setParent(pHandle)
|
||||
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
logger.verbose("Item '%s' is a root item", str(tHandle))
|
||||
if nwItem.isRootType():
|
||||
logger.debug("Item '%s' is a root item", str(tHandle))
|
||||
self._treeRoots[tHandle] = nwItem
|
||||
if nwItem.itemClass == nwItemClass.ARCHIVE:
|
||||
logger.verbose("Item '%s' is the archive folder", str(tHandle))
|
||||
logger.debug("Item '%s' is the archive folder", str(tHandle))
|
||||
self._archRoot = tHandle
|
||||
elif nwItem.itemClass == nwItemClass.TRASH:
|
||||
if self._trashRoot is None:
|
||||
logger.verbose("Item '%s' is the trash folder", str(tHandle))
|
||||
logger.debug("Item '%s' is the trash folder", str(tHandle))
|
||||
self._trashRoot = tHandle
|
||||
else:
|
||||
logger.error("Only one trash folder allowed")
|
||||
@@ -114,30 +113,25 @@ class NWTree():
|
||||
|
||||
return True
|
||||
|
||||
def packXML(self, xParent):
|
||||
def pack(self):
|
||||
"""Pack the content of the tree into the provided XML object. In
|
||||
the order defined by the _treeOrder list.
|
||||
"""
|
||||
xContent = etree.SubElement(xParent, "content", attrib={
|
||||
"count": str(len(self._treeOrder))}
|
||||
)
|
||||
tree = []
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self.__getitem__(tHandle)
|
||||
tItem.packXML(xContent)
|
||||
return
|
||||
if tItem:
|
||||
tree.append(tItem.pack())
|
||||
return tree
|
||||
|
||||
def unpackXML(self, xContent):
|
||||
"""Iterate through all items of a content XML object and add
|
||||
them to the project tree.
|
||||
def unpack(self, data):
|
||||
"""Iterate through all items of a list and add them to the
|
||||
project tree.
|
||||
"""
|
||||
if xContent.tag != "content":
|
||||
logger.error("XML entry is not a NWTree")
|
||||
return False
|
||||
|
||||
self.clear()
|
||||
for xItem in xContent:
|
||||
for item in data:
|
||||
nwItem = NWItem(self.theProject)
|
||||
if nwItem.unpackXML(xItem):
|
||||
if nwItem.unpack(item):
|
||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||
nwItem.saveInitialCount()
|
||||
|
||||
@@ -147,16 +141,22 @@ class NWTree():
|
||||
"""Write the convenience table of contents file in the root of
|
||||
the project directory.
|
||||
"""
|
||||
runtimePath = self.theProject.storage.runtimePath
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
|
||||
return False
|
||||
|
||||
tocList = []
|
||||
tocLen = 0
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
continue
|
||||
|
||||
tFile = tHandle+".nwd"
|
||||
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
|
||||
if (contentPath / tFile).is_file():
|
||||
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
||||
os.path.join("content", tFile),
|
||||
str(Path("content") / tFile),
|
||||
tItem.itemClass.name,
|
||||
tItem.itemLayout.name,
|
||||
tItem.itemName,
|
||||
@@ -166,7 +166,7 @@ class NWTree():
|
||||
|
||||
try:
|
||||
# Dump the text
|
||||
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
|
||||
tocText = runtimePath / nwFiles.TOC_TXT
|
||||
with open(tocText, mode="w", encoding="utf-8") as outFile:
|
||||
outFile.write("\n")
|
||||
outFile.write("Table of Contents\n")
|
||||
@@ -274,11 +274,13 @@ class NWTree():
|
||||
return rootClasses
|
||||
|
||||
def iterRoots(self, itemClass):
|
||||
"""Iterate over all items of a given class.
|
||||
"""Iterate over all root items of a given class in order.
|
||||
"""
|
||||
for tHandle, nwItem in self._treeRoots.items():
|
||||
if nwItem.itemClass == itemClass:
|
||||
yield tHandle, nwItem
|
||||
for tHandle in self._treeOrder:
|
||||
nwItem = self.__getitem__(tHandle)
|
||||
if nwItem is not None and nwItem.isRootType():
|
||||
if itemClass is None or nwItem.itemClass == itemClass:
|
||||
yield tHandle, nwItem
|
||||
return
|
||||
|
||||
def isRoot(self, tHandle):
|
||||
@@ -329,25 +331,20 @@ class NWTree():
|
||||
def setOrder(self, newOrder):
|
||||
"""Reorders the tree based on a list of items.
|
||||
"""
|
||||
tmpOrder = []
|
||||
|
||||
# Add all known elements to a new temp list
|
||||
for tHandle in newOrder:
|
||||
if tHandle in self._projTree:
|
||||
tmpOrder.append(tHandle)
|
||||
else:
|
||||
logger.error("Handle '%s' in new tree order is not in project tree", tHandle)
|
||||
|
||||
# Do a reverse lookup to check for items that will be lost
|
||||
# This is mainly for debugging purposes
|
||||
for tHandle in self._treeOrder:
|
||||
if tHandle not in tmpOrder:
|
||||
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
|
||||
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
|
||||
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
|
||||
# Something is wrong, so let's debug it
|
||||
for tHandle in newOrder:
|
||||
if tHandle not in self._projTree:
|
||||
logger.error("Handle '%s' in new tree order is not in old order", tHandle)
|
||||
for tHandle in self._treeOrder:
|
||||
if tHandle not in tmpOrder:
|
||||
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
||||
|
||||
# Save the temp list
|
||||
self._treeOrder = tmpOrder
|
||||
self._setTreeChanged(True)
|
||||
logger.verbose("Project tree order updated")
|
||||
logger.debug("Project tree order updated")
|
||||
|
||||
return
|
||||
|
||||
@@ -357,7 +354,7 @@ class NWTree():
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
if not tItem.isFileType():
|
||||
logger.error("Item '%s' is not a file", tHandle)
|
||||
return False
|
||||
if not isinstance(itemLayout, nwItemLayout):
|
||||
@@ -457,7 +454,7 @@ class NWTree():
|
||||
"""Generate a unique item handle. In the event that the key
|
||||
already exists, generate a new one.
|
||||
"""
|
||||
logger.verbose("Generating new handle")
|
||||
logger.debug("Generating new handle")
|
||||
handle = f"{random.getrandbits(52):013x}"
|
||||
if handle in self._projTree:
|
||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||
|
||||
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
|
||||
class QHelpLabel(QLabel):
|
||||
|
||||
def __init__(self, theText, textCol, fontSize=0.9):
|
||||
QLabel.__init__(self, theText)
|
||||
super().__init__(theText)
|
||||
|
||||
if isinstance(textCol, QColor):
|
||||
qCol = textCol
|
||||
@@ -267,8 +267,8 @@ class QSwitch(QAbstractButton):
|
||||
return self._offset
|
||||
|
||||
@offset.setter
|
||||
def offset(self, theOffset):
|
||||
self._offset = theOffset
|
||||
def offset(self, offset):
|
||||
self._offset = offset
|
||||
self.update()
|
||||
return
|
||||
|
||||
@@ -276,11 +276,11 @@ class QSwitch(QAbstractButton):
|
||||
# Getters and Setters
|
||||
##
|
||||
|
||||
def setChecked(self, isChecked):
|
||||
def setChecked(self, checked):
|
||||
"""Overload setChecked to also alter the offset.
|
||||
"""
|
||||
super().setChecked(isChecked)
|
||||
if isChecked:
|
||||
super().setChecked(checked)
|
||||
if checked:
|
||||
self.offset = self._xW - self._xR
|
||||
else:
|
||||
self.offset = self._xR
|
||||
@@ -290,10 +290,10 @@ class QSwitch(QAbstractButton):
|
||||
# Events
|
||||
##
|
||||
|
||||
def resizeEvent(self, theEvent):
|
||||
def resizeEvent(self, event):
|
||||
"""Overload resize to ensure correct offset.
|
||||
"""
|
||||
super().resizeEvent(theEvent)
|
||||
super().resizeEvent(event)
|
||||
if self.isChecked():
|
||||
self.offset = self._xW - self._xR
|
||||
else:
|
||||
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
|
||||
class PagedDialog(QDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._tabBar = VerticalTabBar(self)
|
||||
self._tabBar.setExpanding(False)
|
||||
@@ -410,31 +410,37 @@ class PagedDialog(QDialog):
|
||||
return
|
||||
|
||||
def addTab(self, widget, label):
|
||||
"""Forwards the adding of tabs to the QTabWidget.
|
||||
"""Forward the adding of tabs to the QTabWidget.
|
||||
"""
|
||||
self._tabBox.addTab(widget, label)
|
||||
return
|
||||
|
||||
def addControls(self, buttonBar):
|
||||
"""Adds a button bar to the dialog.
|
||||
"""Add a button bar to the dialog.
|
||||
"""
|
||||
self._buttonBox.addWidget(buttonBar)
|
||||
return
|
||||
|
||||
def setCurrentWidget(self, widget):
|
||||
"""Forward the changing of tab to the QTabWidget.
|
||||
"""
|
||||
self._tabBox.setCurrentWidget(widget)
|
||||
return
|
||||
|
||||
# END Class PagedDialog
|
||||
|
||||
|
||||
class VerticalTabBar(QTabBar):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTabBar.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
self._mW = novelwriter.CONFIG.pxInt(150)
|
||||
return
|
||||
|
||||
def tabSizeHint(self, index):
|
||||
"""Returns a transposed size hint for the rotated bar.
|
||||
"""Return a transposed size hint for the rotated bar.
|
||||
"""
|
||||
tSize = QTabBar.tabSizeHint(self, index)
|
||||
tSize = super().tabSizeHint(index)
|
||||
tSize.transpose()
|
||||
tSize.setWidth(min(tSize.width(), self._mW))
|
||||
return tSize
|
||||
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiAbout(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiAbout ...")
|
||||
self.setObjectName("GuiAbout")
|
||||
@@ -234,7 +233,7 @@ class GuiAbout(QDialog):
|
||||
def _fillNotesPage(self):
|
||||
"""Load the content for the Release Notes page.
|
||||
"""
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
|
||||
docPath = self.mainConf.assetPath("text") / "release_notes.htm"
|
||||
docText = readTextFile(docPath)
|
||||
if docText:
|
||||
self.pageNotes.setHtml(docText)
|
||||
@@ -245,7 +244,7 @@ class GuiAbout(QDialog):
|
||||
def _fillLicensePage(self):
|
||||
"""Load the content for the Licence page.
|
||||
"""
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
|
||||
docPath = self.mainConf.assetPath("text") / "gplv3_en.htm"
|
||||
docText = readTextFile(docPath)
|
||||
if docText:
|
||||
self.pageLicense.setHtml(docText)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
novelWriter – GUI Doc Merge Tool
|
||||
================================
|
||||
GUI class for merging multiple documents to one document
|
||||
novelWriter – GUI Doc Merge Dialog
|
||||
==================================
|
||||
Custom dialog class for merging documents.
|
||||
|
||||
File History:
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
Rewritten: 2022-10-06 [2.0b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
@@ -26,169 +27,146 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
|
||||
QListWidgetItem, QDialogButtonBox
|
||||
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
||||
QListWidget, QListWidgetItem, QVBoxLayout,
|
||||
)
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwAlert, nwItemType
|
||||
from novelwriter.gui.custom import QHelpLabel
|
||||
from novelwriter.custom import QHelpLabel, QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocMerge(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
def __init__(self, mainGui, sHandle, itemList):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocMerge ...")
|
||||
self.setObjectName("GuiDocMerge")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = mainGui.theProject
|
||||
self.sourceItem = None
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self._data = {}
|
||||
|
||||
self.setWindowTitle(self.tr("Merge Documents"))
|
||||
|
||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
||||
self.helpLabel = QHelpLabel(
|
||||
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
|
||||
)
|
||||
self.helpLabel = QHelpLabel(self.tr(
|
||||
"Drag and drop items to change the order, or uncheck to exclude."
|
||||
), self.mainTheme.helpText)
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hSp = self.mainConf.pxInt(12)
|
||||
vSp = self.mainConf.pxInt(8)
|
||||
bSp = self.mainConf.pxInt(12)
|
||||
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
self.listBox.setIconSize(QSize(iPx, iPx))
|
||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
||||
self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
|
||||
# Merge Options
|
||||
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
|
||||
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
|
||||
self.optBox = QGridLayout()
|
||||
self.optBox.addWidget(self.trashLabel, 0, 0)
|
||||
self.optBox.addWidget(self.trashSwitch, 0, 1)
|
||||
self.optBox.setHorizontalSpacing(hSp)
|
||||
self.optBox.setColumnStretch(2, 1)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doMerge)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
|
||||
self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
|
||||
self.resetButton.clicked.connect(self._resetList)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(0)
|
||||
self.outerBox.addWidget(self.headLabel)
|
||||
self.outerBox.addWidget(self.helpLabel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addWidget(self.listBox)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addLayout(self.optBox)
|
||||
self.outerBox.addSpacing(bSp)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.rejected.connect(self._doClose)
|
||||
|
||||
self._populateList()
|
||||
# Load Content
|
||||
self._loadContent(sHandle, itemList)
|
||||
|
||||
logger.debug("GuiDocMerge initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Buttons
|
||||
##
|
||||
|
||||
def _doMerge(self):
|
||||
"""Perform the merge of the files in the selected folder, and
|
||||
create a new file in the same parent folder. The old files are
|
||||
not removed in the merge process, and must be deleted manually.
|
||||
def getData(self):
|
||||
"""Return the user's choices.
|
||||
"""
|
||||
logger.verbose("GuiDocMerge merge button clicked")
|
||||
|
||||
finalOrder = []
|
||||
finalItems = []
|
||||
for i in range(self.listBox.count()):
|
||||
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
|
||||
item = self.listBox.item(i)
|
||||
if item is not None and item.checkState() == Qt.Checked:
|
||||
finalItems.append(item.data(Qt.UserRole))
|
||||
|
||||
if len(finalOrder) == 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source documents found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
self._data["moveToTrash"] = self.trashSwitch.isChecked()
|
||||
self._data["finalItems"] = finalItems
|
||||
|
||||
theText = ""
|
||||
for tHandle in finalOrder:
|
||||
inDoc = NWDoc(self.theProject, tHandle)
|
||||
docText = inDoc.readDocument()
|
||||
docErr = inDoc.getError()
|
||||
if docText is None and docErr:
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
if docText:
|
||||
theText += docText.rstrip("\n")+"\n\n"
|
||||
return self._data
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source folder selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
srcItem = self.theProject.tree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
|
||||
newItem = self.theProject.tree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
newItem.setImport(srcItem.itemImport)
|
||||
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
||||
self.mainGui.openDocument(nHandle, doScroll=True)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
def _resetList(self):
|
||||
"""Reset the content of the list box to its original state.
|
||||
"""
|
||||
self.close()
|
||||
logger.debug("Resetting list box content")
|
||||
sHandle = self._data.get("sHandle", None)
|
||||
itemList = self._data.get("origItems", [])
|
||||
self._loadContent(sHandle, itemList)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _populateList(self):
|
||||
"""Get the item selected in the tree, check that it is a folder,
|
||||
and try to find all files associated with it. The valid files
|
||||
are then added to the list view in order. The list itself can be
|
||||
reordered by the user.
|
||||
def _loadContent(self, sHandle, itemList):
|
||||
"""Load content from a given list of items.
|
||||
"""
|
||||
tHandle = self.mainGui.projView.getSelectedHandle()
|
||||
self.sourceItem = tHandle
|
||||
if tHandle is None:
|
||||
return False
|
||||
self._data = {}
|
||||
self._data["sHandle"] = sHandle
|
||||
self._data["origItems"] = itemList
|
||||
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
if nwItem is None:
|
||||
return False
|
||||
|
||||
if nwItem.itemType is not nwItemType.FOLDER:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Element selected in the project tree must be a folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
|
||||
newItem = QListWidgetItem()
|
||||
nwItem = self.theProject.tree[sHandle]
|
||||
if nwItem.itemType is not nwItemType.FILE:
|
||||
self.listBox.clear()
|
||||
for tHandle in itemList:
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
if nwItem is None or not nwItem.isFileType():
|
||||
continue
|
||||
|
||||
itemIcon = self.mainTheme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||
)
|
||||
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setIcon(itemIcon)
|
||||
newItem.setText(nwItem.itemName)
|
||||
newItem.setData(Qt.UserRole, sHandle)
|
||||
newItem.setData(Qt.UserRole, tHandle)
|
||||
newItem.setCheckState(Qt.Checked)
|
||||
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
# END Class GuiDocMerge
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
novelWriter – GUI Doc Split Tool
|
||||
================================
|
||||
GUI class for splitting a single document into multiple documents
|
||||
novelWriter – GUI Doc Split Dialog
|
||||
==================================
|
||||
Custom dialog class for splitting documents.
|
||||
|
||||
File History:
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
Rewritten: 2022-10-12 [2.0b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
@@ -29,32 +30,34 @@ import novelwriter
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
||||
QListWidgetItem, QDialogButtonBox, QLabel
|
||||
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
|
||||
)
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwAlert, nwItemType
|
||||
from novelwriter.gui.custom import QHelpLabel
|
||||
from novelwriter.custom import QHelpLabel, QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocSplit(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
LINE_ROLE = Qt.UserRole
|
||||
LEVEL_ROLE = Qt.UserRole + 1
|
||||
LABEL_ROLE = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, mainGui, sHandle):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocSplit ...")
|
||||
self.setObjectName("GuiDocSplit")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = mainGui.theProject
|
||||
|
||||
self.sourceItem = None
|
||||
self.sourceText = []
|
||||
self._data = {}
|
||||
self._text = []
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.setWindowTitle(self.tr("Split Document"))
|
||||
|
||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
||||
@@ -63,6 +66,18 @@ class GuiDocSplit(QDialog):
|
||||
self.mainGui.mainTheme.helpText
|
||||
)
|
||||
|
||||
# Values
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hSp = self.mainConf.pxInt(12)
|
||||
vSp = self.mainConf.pxInt(8)
|
||||
bSp = self.mainConf.pxInt(12)
|
||||
|
||||
pOptions = self.theProject.options
|
||||
spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
|
||||
intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
|
||||
docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
|
||||
|
||||
# Header Selection
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||
@@ -73,206 +88,162 @@ class GuiDocSplit(QDialog):
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
||||
spIndex = self.splitLevel.findData(
|
||||
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
|
||||
)
|
||||
spIndex = self.splitLevel.findData(spLevel)
|
||||
if spIndex != -1:
|
||||
self.splitLevel.setCurrentIndex(spIndex)
|
||||
self.splitLevel.currentIndexChanged.connect(self._populateList)
|
||||
self.splitLevel.currentIndexChanged.connect(self._reloadList)
|
||||
|
||||
# Split Options
|
||||
self.folderLabel = QLabel(self.tr("Split into a new folder"))
|
||||
self.folderSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
self.folderSwitch.setChecked(intoFolder)
|
||||
|
||||
self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"))
|
||||
self.hierarchySwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
self.hierarchySwitch.setChecked(docHierarchy)
|
||||
|
||||
self.trashLabel = QLabel(self.tr("Move split document to Trash"))
|
||||
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
|
||||
self.optBox = QGridLayout()
|
||||
self.optBox.addWidget(self.folderLabel, 0, 0)
|
||||
self.optBox.addWidget(self.folderSwitch, 0, 1)
|
||||
self.optBox.addWidget(self.hierarchyLabel, 1, 0)
|
||||
self.optBox.addWidget(self.hierarchySwitch, 1, 1)
|
||||
self.optBox.addWidget(self.trashLabel, 2, 0)
|
||||
self.optBox.addWidget(self.trashSwitch, 2, 1)
|
||||
self.optBox.setVerticalSpacing(vSp)
|
||||
self.optBox.setHorizontalSpacing(hSp)
|
||||
self.optBox.setColumnStretch(3, 1)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doSplit)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(0)
|
||||
self.outerBox.addWidget(self.headLabel)
|
||||
self.outerBox.addWidget(self.helpLabel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addWidget(self.listBox)
|
||||
self.outerBox.addWidget(self.splitLevel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addLayout(self.optBox)
|
||||
self.outerBox.addSpacing(bSp)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.rejected.connect(self._doClose)
|
||||
|
||||
self._populateList()
|
||||
# Load Content
|
||||
self._loadContent(sHandle)
|
||||
|
||||
logger.debug("GuiDocSplit initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Buttons
|
||||
##
|
||||
|
||||
def _doSplit(self):
|
||||
"""Perform the split of the file, create a new folder in the
|
||||
same parent folder, and multiple files depending on split level
|
||||
settings. The old file is not removed in the split process, and
|
||||
must be deleted manually.
|
||||
def getData(self):
|
||||
"""Return the user's choices. Also save the users options for
|
||||
the next time the dialog is used.
|
||||
"""
|
||||
logger.verbose("GuiDocSplit split button clicked")
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source document selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
srcItem = self.theProject.tree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
||||
theText = inDoc.readDocument()
|
||||
|
||||
docErr = inDoc.getError()
|
||||
if theText is None and docErr:
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
|
||||
if theText is None:
|
||||
theText = ""
|
||||
|
||||
nLines = len(self.sourceText)
|
||||
logger.debug("Splitting document %s with %d lines", self.sourceItem, nLines)
|
||||
|
||||
finalOrder = []
|
||||
headerList = []
|
||||
for i in range(self.listBox.count()):
|
||||
listItem = self.listBox.item(i)
|
||||
wTitle = listItem.text()
|
||||
lineNo = listItem.data(Qt.UserRole)
|
||||
finalOrder.append([wTitle, lineNo, nLines])
|
||||
if i > 0:
|
||||
finalOrder[i-1][2] = lineNo
|
||||
item = self.listBox.item(i)
|
||||
if item is not None:
|
||||
headerList.append((
|
||||
item.data(self.LINE_ROLE),
|
||||
item.data(self.LEVEL_ROLE),
|
||||
item.data(self.LABEL_ROLE),
|
||||
))
|
||||
|
||||
nFiles = len(finalOrder)
|
||||
if nFiles == 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No headers found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
spLevel = self.splitLevel.currentData()
|
||||
intoFolder = self.folderSwitch.isChecked()
|
||||
docHierarchy = self.hierarchySwitch.isChecked()
|
||||
moveToTrash = self.trashSwitch.isChecked()
|
||||
|
||||
msgYes = self.mainGui.askQuestion(
|
||||
self.tr("Split Document"),
|
||||
"{0}<br><br>{1}".format(
|
||||
self.tr(
|
||||
"The document will be split into {0} file(s) in a new folder. "
|
||||
"The original document will remain intact."
|
||||
).format(nFiles),
|
||||
self.tr(
|
||||
"Continue with the splitting process?"
|
||||
)
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
self._data["spLevel"] = spLevel
|
||||
self._data["headerList"] = headerList
|
||||
self._data["intoFolder"] = intoFolder
|
||||
self._data["docHierarchy"] = docHierarchy
|
||||
self._data["moveToTrash"] = moveToTrash
|
||||
|
||||
# Create the folder
|
||||
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
|
||||
self.mainGui.projView.revealNewTreeItem(fHandle)
|
||||
logger.verbose("Creating folder '%s'", fHandle)
|
||||
pOptions = self.theProject.options
|
||||
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
|
||||
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
|
||||
|
||||
# Loop through, and create the files
|
||||
for wTitle, iStart, iEnd in finalOrder:
|
||||
return self._data, self._text
|
||||
|
||||
wTitle = wTitle.lstrip("#").strip()
|
||||
nHandle = self.theProject.newFile(wTitle, fHandle)
|
||||
newItem = self.theProject.tree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
newItem.setImport(srcItem.itemImport)
|
||||
logger.verbose(
|
||||
"Creating new document '%s' with text from line %d to %d",
|
||||
nHandle, iStart+1, iEnd
|
||||
)
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
||||
theText = theText.rstrip("\n") + "\n\n"
|
||||
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
def _reloadList(self):
|
||||
"""Reload the content of the list box.
|
||||
"""
|
||||
self.theProject.options.saveSettings()
|
||||
self.close()
|
||||
sHandle = self._data.get("sHandle", None)
|
||||
self._loadContent(sHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _populateList(self):
|
||||
"""Get the item selected in the tree, check that it is a folder,
|
||||
and try to find all files associated with it. The valid files
|
||||
are then added to the list view in order. The list itself can be
|
||||
reordered by the user.
|
||||
def _loadContent(self, sHandle):
|
||||
"""Load content from a given source item.
|
||||
"""
|
||||
self._data = {}
|
||||
self._data["sHandle"] = sHandle
|
||||
|
||||
self.listBox.clear()
|
||||
if self.sourceItem is None:
|
||||
self.sourceItem = self.mainGui.projView.getSelectedHandle()
|
||||
|
||||
if self.sourceItem is None:
|
||||
return False
|
||||
|
||||
nwItem = self.theProject.tree[self.sourceItem]
|
||||
if nwItem is None:
|
||||
return False
|
||||
|
||||
if nwItem.itemType is not nwItemType.FILE:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Element selected in the project tree must be a file."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
||||
theText = inDoc.readDocument()
|
||||
if theText is None:
|
||||
theText = ""
|
||||
return False
|
||||
nwItem = self.theProject.tree[sHandle]
|
||||
if nwItem is None or not nwItem.isFileType():
|
||||
return
|
||||
|
||||
spLevel = self.splitLevel.currentData()
|
||||
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
logger.debug(
|
||||
"Scanning document '%s' for headings level <= %d",
|
||||
self.sourceItem, spLevel
|
||||
)
|
||||
if not self._text:
|
||||
inDoc = self.theProject.storage.getDocument(sHandle)
|
||||
self._text = (inDoc.readDocument() or "").splitlines()
|
||||
|
||||
self.sourceText = theText.splitlines()
|
||||
for lineNo, aLine in enumerate(self.sourceText):
|
||||
for lineNo, aLine in enumerate(self._text):
|
||||
|
||||
onLine = -1
|
||||
hLevel = 0
|
||||
hLabel = aLine.strip()
|
||||
if aLine.startswith("# ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
hLevel = 1
|
||||
hLabel = aLine[2:].strip()
|
||||
elif aLine.startswith("## ") and spLevel >= 2:
|
||||
onLine = lineNo
|
||||
hLevel = 2
|
||||
hLabel = aLine[3:].strip()
|
||||
elif aLine.startswith("### ") and spLevel >= 3:
|
||||
onLine = lineNo
|
||||
hLevel = 3
|
||||
hLabel = aLine[4:].strip()
|
||||
elif aLine.startswith("#### ") and spLevel >= 4:
|
||||
onLine = lineNo
|
||||
hLevel = 4
|
||||
hLabel = aLine[5:].strip()
|
||||
elif aLine.startswith("#! ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
hLevel = 1
|
||||
hLabel = aLine[3:].strip()
|
||||
elif aLine.startswith("##! ") and spLevel >= 2:
|
||||
onLine = lineNo
|
||||
hLevel = 2
|
||||
hLabel = aLine[4:].strip()
|
||||
|
||||
if onLine >= 0:
|
||||
if onLine >= 0 and hLevel > 0:
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setText(aLine.strip())
|
||||
newItem.setData(Qt.UserRole, onLine)
|
||||
newItem.setData(self.LINE_ROLE, onLine)
|
||||
newItem.setData(self.LEVEL_ROLE, hLevel)
|
||||
newItem.setData(self.LABEL_ROLE, hLabel)
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
# END Class GuiDocSplit
|
||||
|
||||
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiEditLabel(QDialog):
|
||||
|
||||
def __init__(self, parent, text=""):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setObjectName("GuiEditLabel")
|
||||
self.setWindowTitle(self.tr("Item Label"))
|
||||
|
||||
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
@@ -34,8 +33,7 @@ from PyQt5.QtWidgets import (
|
||||
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.gui.custom import QSwitch, QConfigLayout, PagedDialog
|
||||
from novelwriter.custom import QSwitch, QConfigLayout, PagedDialog
|
||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiPreferences(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiPreferences ...")
|
||||
self.setObjectName("GuiPreferences")
|
||||
@@ -55,13 +53,13 @@ class GuiPreferences(PagedDialog):
|
||||
|
||||
self.setWindowTitle(self.tr("Preferences"))
|
||||
|
||||
self.tabGeneral = GuiPreferencesGeneral(self.mainGui)
|
||||
self.tabProjects = GuiPreferencesProjects(self.mainGui)
|
||||
self.tabDocs = GuiPreferencesDocuments(self.mainGui)
|
||||
self.tabEditor = GuiPreferencesEditor(self.mainGui)
|
||||
self.tabSyntax = GuiPreferencesSyntax(self.mainGui)
|
||||
self.tabAuto = GuiPreferencesAutomation(self.mainGui)
|
||||
self.tabQuote = GuiPreferencesQuotes(self.mainGui)
|
||||
self.tabGeneral = GuiPreferencesGeneral(self)
|
||||
self.tabProjects = GuiPreferencesProjects(self)
|
||||
self.tabDocs = GuiPreferencesDocuments(self)
|
||||
self.tabEditor = GuiPreferencesEditor(self)
|
||||
self.tabSyntax = GuiPreferencesSyntax(self)
|
||||
self.tabAuto = GuiPreferencesAutomation(self)
|
||||
self.tabQuote = GuiPreferencesQuotes(self)
|
||||
|
||||
self.addTab(self.tabGeneral, self.tr("General"))
|
||||
self.addTab(self.tabProjects, self.tr("Projects"))
|
||||
@@ -76,12 +74,38 @@ class GuiPreferences(PagedDialog):
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
self.resize(*self.mainConf.getPreferencesSize())
|
||||
self.resize(*self.mainConf.preferencesWinSize)
|
||||
|
||||
# Settings
|
||||
self._updateTheme = False
|
||||
self._updateSyntax = False
|
||||
self._needsRestart = False
|
||||
self._refreshTree = False
|
||||
|
||||
logger.debug("GuiPreferences initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def updateTheme(self):
|
||||
return self._updateTheme
|
||||
|
||||
@property
|
||||
def updateSyntax(self):
|
||||
return self._updateSyntax
|
||||
|
||||
@property
|
||||
def needsRestart(self):
|
||||
return self._needsRestart
|
||||
|
||||
@property
|
||||
def refreshTree(self):
|
||||
return self._refreshTree
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -92,8 +116,7 @@ class GuiPreferences(PagedDialog):
|
||||
"""
|
||||
logger.debug("Saving new preferences")
|
||||
|
||||
needsRestart, refreshTree = self.tabGeneral.saveValues()
|
||||
|
||||
self.tabGeneral.saveValues()
|
||||
self.tabProjects.saveValues()
|
||||
self.tabDocs.saveValues()
|
||||
self.tabEditor.saveValues()
|
||||
@@ -101,15 +124,8 @@ class GuiPreferences(PagedDialog):
|
||||
self.tabAuto.saveValues()
|
||||
self.tabQuote.saveValues()
|
||||
|
||||
if needsRestart:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Some changes will not be applied until novelWriter has been restarted."
|
||||
), nwAlert.INFO)
|
||||
|
||||
if refreshTree:
|
||||
self.mainGui.projView.populateTree()
|
||||
|
||||
self._saveWindowSize()
|
||||
self.mainConf.saveConfig()
|
||||
self.accept()
|
||||
|
||||
return
|
||||
@@ -128,9 +144,7 @@ class GuiPreferences(PagedDialog):
|
||||
def _saveWindowSize(self):
|
||||
"""Save the dialog window size.
|
||||
"""
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
self.mainConf.setPreferencesSize(winWidth, winHeight)
|
||||
self.mainConf.setPreferencesWinSize(self.width(), self.height())
|
||||
return
|
||||
|
||||
# END Class GuiPreferences
|
||||
@@ -138,12 +152,13 @@ class GuiPreferences(PagedDialog):
|
||||
|
||||
class GuiPreferencesGeneral(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.prefsGui = prefsGui
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -156,19 +171,19 @@ class GuiPreferencesGeneral(QWidget):
|
||||
minWidth = self.mainConf.pxInt(200)
|
||||
|
||||
# Select Locale
|
||||
self.guiLang = QComboBox()
|
||||
self.guiLang.setMinimumWidth(minWidth)
|
||||
self.guiLocale = QComboBox()
|
||||
self.guiLocale.setMinimumWidth(minWidth)
|
||||
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW)
|
||||
for lang, langName in theLangs:
|
||||
self.guiLang.addItem(langName, lang)
|
||||
langIdx = self.guiLang.findData(self.mainConf.guiLang)
|
||||
self.guiLocale.addItem(langName, lang)
|
||||
langIdx = self.guiLocale.findData(self.mainConf.guiLocale)
|
||||
if langIdx != -1:
|
||||
self.guiLang.setCurrentIndex(langIdx)
|
||||
self.guiLocale.setCurrentIndex(langIdx)
|
||||
|
||||
self.mainForm.addRow(
|
||||
self.tr("Main GUI language"),
|
||||
self.guiLang,
|
||||
self.tr("Requires restart.")
|
||||
self.guiLocale,
|
||||
self.tr("Requires restart to take effect.")
|
||||
)
|
||||
|
||||
# Select Theme
|
||||
@@ -184,23 +199,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Main GUI theme"),
|
||||
self.guiTheme,
|
||||
self.tr("Requires restart.")
|
||||
)
|
||||
|
||||
# Select Icon Theme
|
||||
self.guiIcons = QComboBox()
|
||||
self.guiIcons.setMinimumWidth(minWidth)
|
||||
self.iconCache = self.mainTheme.iconCache.listThemes()
|
||||
for iconDir, iconName in self.iconCache:
|
||||
self.guiIcons.addItem(iconName, iconDir)
|
||||
iconIdx = self.guiIcons.findData(self.mainConf.guiIcons)
|
||||
if iconIdx != -1:
|
||||
self.guiIcons.setCurrentIndex(iconIdx)
|
||||
|
||||
self.mainForm.addRow(
|
||||
self.tr("Main icon theme"),
|
||||
self.guiIcons,
|
||||
self.tr("Requires restart.")
|
||||
self.tr("General colour theme and icons.")
|
||||
)
|
||||
|
||||
# Editor Theme
|
||||
@@ -230,7 +229,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Font family"),
|
||||
self.guiFont,
|
||||
self.tr("Requires restart."),
|
||||
self.tr("Requires restart to take effect."),
|
||||
theButton=self.fontButton
|
||||
)
|
||||
|
||||
@@ -243,7 +242,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Font size"),
|
||||
self.guiFontSize,
|
||||
self.tr("Requires restart."),
|
||||
self.tr("Requires restart to take effect."),
|
||||
theUnit=self.tr("pt")
|
||||
)
|
||||
|
||||
@@ -288,29 +287,23 @@ class GuiPreferencesGeneral(QWidget):
|
||||
def saveValues(self):
|
||||
"""Save the values set for this tab.
|
||||
"""
|
||||
guiLang = self.guiLang.currentData()
|
||||
guiLocale = self.guiLocale.currentData()
|
||||
guiTheme = self.guiTheme.currentData()
|
||||
guiIcons = self.guiIcons.currentData()
|
||||
guiSyntax = self.guiSyntax.currentData()
|
||||
guiFont = self.guiFont.text()
|
||||
guiFontSize = self.guiFontSize.value()
|
||||
emphLabels = self.emphLabels.isChecked()
|
||||
|
||||
# Check if restart is needed
|
||||
needsRestart = False
|
||||
needsRestart |= self.mainConf.guiLang != guiLang
|
||||
needsRestart |= self.mainConf.guiTheme != guiTheme
|
||||
needsRestart |= self.mainConf.guiIcons != guiIcons
|
||||
needsRestart |= self.mainConf.guiFont != guiFont
|
||||
needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
||||
# Update Flags
|
||||
self.prefsGui._updateTheme |= self.mainConf.guiTheme != guiTheme
|
||||
self.prefsGui._updateSyntax |= self.mainConf.guiSyntax != guiSyntax
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiLocale != guiLocale
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiFont != guiFont
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
||||
self.prefsGui._refreshTree |= self.mainConf.emphLabels != emphLabels
|
||||
|
||||
# Check if refreshing project tree is needed
|
||||
refreshTree = False
|
||||
refreshTree |= self.mainConf.emphLabels != emphLabels
|
||||
|
||||
self.mainConf.guiLang = guiLang
|
||||
self.mainConf.guiLocale = guiLocale
|
||||
self.mainConf.guiTheme = guiTheme
|
||||
self.mainConf.guiIcons = guiIcons
|
||||
self.mainConf.guiSyntax = guiSyntax
|
||||
self.mainConf.guiFont = guiFont
|
||||
self.mainConf.guiFontSize = guiFontSize
|
||||
@@ -319,9 +312,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainConf.hideVScroll = self.hideVScroll.isChecked()
|
||||
self.mainConf.hideHScroll = self.hideHScroll.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return needsRestart, refreshTree
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
@@ -344,12 +335,12 @@ class GuiPreferencesGeneral(QWidget):
|
||||
|
||||
class GuiPreferencesProjects(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -391,7 +382,7 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainForm.addGroupLabel(self.tr("Project Backup"))
|
||||
|
||||
# Backup Path
|
||||
self.backupPath = self.mainConf.backupPath
|
||||
self.backupPath = self.mainConf.backupPath()
|
||||
self.backupGetPath = QPushButton(self.tr("Browse"))
|
||||
self.backupGetPath.clicked.connect(self._backupFolder)
|
||||
self.backupPathRow = self.mainForm.addRow(
|
||||
@@ -458,7 +449,7 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainConf.autoSaveProj = self.autoSaveProj.value()
|
||||
|
||||
# Project Backup
|
||||
self.mainConf.backupPath = self.backupPath
|
||||
self.mainConf.setBackupPath(self.backupPath)
|
||||
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
|
||||
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
|
||||
|
||||
@@ -466,8 +457,6 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
|
||||
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -477,12 +466,9 @@ class GuiPreferencesProjects(QWidget):
|
||||
def _backupFolder(self):
|
||||
"""Open a dialog to select the backup folder.
|
||||
"""
|
||||
currDir = self.backupPath
|
||||
if not os.path.isdir(currDir):
|
||||
currDir = ""
|
||||
|
||||
currDir = self.backupPath or ""
|
||||
newDir = QFileDialog.getExistingDirectory(
|
||||
self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly
|
||||
self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
|
||||
)
|
||||
if newDir:
|
||||
self.backupPath = newDir
|
||||
@@ -505,12 +491,12 @@ class GuiPreferencesProjects(QWidget):
|
||||
|
||||
class GuiPreferencesDocuments(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -640,8 +626,6 @@ class GuiPreferencesDocuments(QWidget):
|
||||
self.mainConf.textMargin = self.textMargin.value()
|
||||
self.mainConf.tabWidth = self.tabWidth.value()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -666,12 +650,12 @@ class GuiPreferencesDocuments(QWidget):
|
||||
|
||||
class GuiPreferencesEditor(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -831,8 +815,6 @@ class GuiPreferencesEditor(QWidget):
|
||||
self.mainConf.autoScroll = self.autoScroll.isChecked()
|
||||
self.mainConf.autoScrollPos = self.autoScrollPos.value()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiPreferencesEditor
|
||||
@@ -840,12 +822,12 @@ class GuiPreferencesEditor(QWidget):
|
||||
|
||||
class GuiPreferencesSyntax(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -922,8 +904,6 @@ class GuiPreferencesSyntax(QWidget):
|
||||
# Text Errors
|
||||
self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -943,12 +923,12 @@ class GuiPreferencesSyntax(QWidget):
|
||||
|
||||
class GuiPreferencesAutomation(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -1076,8 +1056,6 @@ class GuiPreferencesAutomation(QWidget):
|
||||
self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip()
|
||||
self.mainConf.fmtPadThin = self.fmtPadThin.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -1100,12 +1078,12 @@ class GuiPreferencesAutomation(QWidget):
|
||||
|
||||
class GuiPreferencesQuotes(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -1126,7 +1104,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["SO"].setReadOnly(True)
|
||||
self.quoteSym["SO"].setFixedWidth(qWidth)
|
||||
self.quoteSym["SO"].setAlignment(Qt.AlignCenter)
|
||||
self.quoteSym["SO"].setText(self.mainConf.fmtSingleQuotes[0])
|
||||
self.quoteSym["SO"].setText(self.mainConf.fmtSQuoteOpen)
|
||||
self.btnSingleStyleO = QPushButton("...")
|
||||
self.btnSingleStyleO.setMaximumWidth(bWidth)
|
||||
self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO"))
|
||||
@@ -1142,7 +1120,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["SC"].setReadOnly(True)
|
||||
self.quoteSym["SC"].setFixedWidth(qWidth)
|
||||
self.quoteSym["SC"].setAlignment(Qt.AlignCenter)
|
||||
self.quoteSym["SC"].setText(self.mainConf.fmtSingleQuotes[1])
|
||||
self.quoteSym["SC"].setText(self.mainConf.fmtSQuoteClose)
|
||||
self.btnSingleStyleC = QPushButton("...")
|
||||
self.btnSingleStyleC.setMaximumWidth(bWidth)
|
||||
self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC"))
|
||||
@@ -1159,7 +1137,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["DO"].setReadOnly(True)
|
||||
self.quoteSym["DO"].setFixedWidth(qWidth)
|
||||
self.quoteSym["DO"].setAlignment(Qt.AlignCenter)
|
||||
self.quoteSym["DO"].setText(self.mainConf.fmtDoubleQuotes[0])
|
||||
self.quoteSym["DO"].setText(self.mainConf.fmtDQuoteOpen)
|
||||
self.btnDoubleStyleO = QPushButton("...")
|
||||
self.btnDoubleStyleO.setMaximumWidth(bWidth)
|
||||
self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO"))
|
||||
@@ -1175,7 +1153,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["DC"].setReadOnly(True)
|
||||
self.quoteSym["DC"].setFixedWidth(qWidth)
|
||||
self.quoteSym["DC"].setAlignment(Qt.AlignCenter)
|
||||
self.quoteSym["DC"].setText(self.mainConf.fmtDoubleQuotes[1])
|
||||
self.quoteSym["DC"].setText(self.mainConf.fmtDQuoteClose)
|
||||
self.btnDoubleStyleC = QPushButton("...")
|
||||
self.btnDoubleStyleC.setMaximumWidth(bWidth)
|
||||
self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC"))
|
||||
@@ -1192,13 +1170,10 @@ class GuiPreferencesQuotes(QWidget):
|
||||
"""Save the values set for this tab.
|
||||
"""
|
||||
# Quotation Style
|
||||
self.mainConf.fmtSingleQuotes[0] = self.quoteSym["SO"].text()
|
||||
self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text()
|
||||
self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text()
|
||||
self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
self.mainConf.fmtSQuoteOpen = self.quoteSym["SO"].text()
|
||||
self.mainConf.fmtSQuoteClose = self.quoteSym["SC"].text()
|
||||
self.mainConf.fmtDQuoteOpen = self.quoteSym["DO"].text()
|
||||
self.mainConf.fmtDQuoteClose = self.quoteSym["DC"].text()
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -27,16 +27,18 @@ import math
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
|
||||
QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.common import numberToRoman
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.gui.custom import PagedDialog, QSwitch
|
||||
from novelwriter.custom import PagedDialog, QSwitch
|
||||
from novelwriter.constants import nwLabels, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +46,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiProjectDetails(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectDetails ...")
|
||||
self.setObjectName("GuiProjectDetails")
|
||||
@@ -140,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
|
||||
class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theProject = theProject
|
||||
@@ -155,7 +157,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
# Header
|
||||
# ======
|
||||
|
||||
self.bookTitle = QLabel(self.theProject.bookTitle)
|
||||
self.bookTitle = QLabel(self.theProject.data.title)
|
||||
bookFont = self.bookTitle.font()
|
||||
bookFont.setPointSizeF(2.2*fPt)
|
||||
bookFont.setWeight(QFont.Bold)
|
||||
@@ -164,7 +166,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.bookTitle.setWordWrap(True)
|
||||
|
||||
self.projName = QLabel(
|
||||
self.tr("Working Title: {0}").format(self.theProject.projName)
|
||||
self.tr("Working Title: {0}").format(self.theProject.data.name)
|
||||
)
|
||||
workFont = self.projName.font()
|
||||
workFont.setPointSizeF(0.8*fPt)
|
||||
@@ -173,7 +175,9 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.projName.setAlignment(Qt.AlignHCenter)
|
||||
self.projName.setWordWrap(True)
|
||||
|
||||
self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors()))
|
||||
self.bookAuthors = QLabel(self.tr("By {0}").format(
|
||||
self.theProject.getFormattedAuthors()
|
||||
))
|
||||
authFont = self.bookAuthors.font()
|
||||
authFont.setPointSizeF(1.2*fPt)
|
||||
self.bookAuthors.setFont(authFont)
|
||||
@@ -253,10 +257,10 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.wordCountVal.setText(f"{nwCount:n}")
|
||||
self.chapCountVal.setText(f"{hCounts[2]:n}")
|
||||
self.sceneCountVal.setText(f"{hCounts[3]:n}")
|
||||
self.revCountVal.setText(f"{self.theProject.saveCount:n}")
|
||||
self.revCountVal.setText(f"{self.theProject.data.saveCount:n}")
|
||||
self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
|
||||
|
||||
self.projPathVal.setText(self.theProject.projPath)
|
||||
self.projPathVal.setText(str(self.theProject.storage.storagePath))
|
||||
|
||||
return
|
||||
|
||||
@@ -272,7 +276,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
C_PROG = 4
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theProject = theProject
|
||||
@@ -281,12 +285,26 @@ class GuiProjectDetailsContents(QWidget):
|
||||
|
||||
# Internal
|
||||
self._theToC = []
|
||||
self._currentRoot = None
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hPx = self.mainConf.pxInt(12)
|
||||
vPx = self.mainConf.pxInt(4)
|
||||
pOptions = self.theProject.options
|
||||
|
||||
# Header
|
||||
# ======
|
||||
|
||||
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
|
||||
|
||||
self.novelValue = QComboBox(self)
|
||||
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
|
||||
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
|
||||
|
||||
self.headBox = QHBoxLayout()
|
||||
self.headBox.addWidget(self.tocLabel)
|
||||
self.headBox.addWidget(self.novelValue)
|
||||
|
||||
# Contents Tree
|
||||
# =============
|
||||
|
||||
@@ -389,7 +407,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
|
||||
self.outerBox.addLayout(self.headBox)
|
||||
self.outerBox.addWidget(self.tocTree)
|
||||
self.outerBox.addLayout(self.optionsBox)
|
||||
|
||||
@@ -412,19 +430,35 @@ class GuiProjectDetailsContents(QWidget):
|
||||
def updateValues(self):
|
||||
"""Populate the tree.
|
||||
"""
|
||||
self._prepareData()
|
||||
self._currentRoot = None
|
||||
self._populateNovelList()
|
||||
|
||||
rootHandle = self.novelValue.currentData()
|
||||
self._prepareData(rootHandle)
|
||||
self._populateTree()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _prepareData(self):
|
||||
"""Extract the data for the tree.
|
||||
def _populateNovelList(self):
|
||||
"""Fill the novel combo box with a list of all novel folders.
|
||||
"""
|
||||
self._theToC = []
|
||||
self._theToC = self.theProject.index.getTableOfContents(2)
|
||||
self.novelValue.clear()
|
||||
|
||||
tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
|
||||
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
|
||||
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
|
||||
|
||||
return
|
||||
|
||||
def _prepareData(self, rootHandle):
|
||||
"""Extract the information from the project index.
|
||||
"""
|
||||
logger.debug("Populating ToC from handle '%s'", rootHandle)
|
||||
self._theToC = self.theProject.index.getTableOfContents(rootHandle, 2)
|
||||
self._theToC.append(("", 0, self.tr("END"), 0))
|
||||
return
|
||||
|
||||
@@ -432,6 +466,18 @@ class GuiProjectDetailsContents(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _novelValueChanged(self):
|
||||
"""Refresh the tree with another root item.
|
||||
"""
|
||||
rootHandle = self.novelValue.currentData()
|
||||
if rootHandle != self._currentRoot:
|
||||
self._prepareData(rootHandle)
|
||||
self._populateTree()
|
||||
self._currentRoot = rootHandle
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _populateTree(self):
|
||||
"""Set the content of the chapter/page tree.
|
||||
"""
|
||||
@@ -466,10 +512,11 @@ class GuiProjectDetailsContents(QWidget):
|
||||
progPage = f"{cPage:n}"
|
||||
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
|
||||
|
||||
hDec = self.mainTheme.getHeaderDecoration(tLevel)
|
||||
if tTitle.strip() == "":
|
||||
tTitle = self.tr("Untitled")
|
||||
|
||||
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel))
|
||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||
newItem.setText(self.C_TITLE, tTitle)
|
||||
newItem.setText(self.C_WORDS, f"{wCount:n}")
|
||||
newItem.setText(self.C_PAGES, f"{pCount:n}")
|
||||
|
||||
@@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
|
||||
C_TIME = 2
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectLoad ...")
|
||||
self.setObjectName("GuiProjectLoad")
|
||||
@@ -77,7 +77,6 @@ class GuiProjectLoad(QDialog):
|
||||
self.setWindowTitle(self.tr("Open Project"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.setModal(True)
|
||||
|
||||
self.nwIcon = QLabel()
|
||||
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
|
||||
@@ -158,7 +157,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doOpenRecent(self):
|
||||
"""Close the dialog window with a recent project selected.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad open button clicked")
|
||||
self._saveSettings()
|
||||
|
||||
self.openPath = None
|
||||
@@ -183,7 +181,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doBrowse(self):
|
||||
"""Browse for a folder path.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad browse button clicked")
|
||||
extFilter = [
|
||||
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
|
||||
self.tr("All files ({0})").format("*"),
|
||||
@@ -192,8 +189,8 @@ class GuiProjectLoad(QDialog):
|
||||
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
|
||||
)
|
||||
if projFile:
|
||||
thePath = os.path.abspath(os.path.dirname(projFile))
|
||||
self.selPath.setText(thePath)
|
||||
thePath = Path(projFile).absolute()
|
||||
self.selPath.setText(str(thePath))
|
||||
self.openPath = thePath
|
||||
self.openState = self.OPEN_STATE
|
||||
self.accept()
|
||||
@@ -203,7 +200,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doCancel(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad close button clicked")
|
||||
self.openPath = None
|
||||
self.openState = self.NONE_STATE
|
||||
self.close()
|
||||
@@ -212,7 +208,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doNewProject(self):
|
||||
"""Create a new project.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad new project button clicked")
|
||||
self._saveSettings()
|
||||
self.openPath = None
|
||||
self.openState = self.NEW_STATE
|
||||
@@ -233,7 +228,7 @@ class GuiProjectLoad(QDialog):
|
||||
).format(projName)
|
||||
)
|
||||
if msgYes:
|
||||
self.mainConf.removeFromRecentCache(
|
||||
self.mainConf.recentProjects.remove(
|
||||
selList[0].data(self.C_NAME, Qt.UserRole)
|
||||
)
|
||||
self._populateList()
|
||||
@@ -262,29 +257,23 @@ class GuiProjectLoad(QDialog):
|
||||
colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
|
||||
colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
|
||||
colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
|
||||
self.mainConf.setProjColWidths(colWidths)
|
||||
self.mainConf.setProjLoadColWidths(colWidths)
|
||||
return
|
||||
|
||||
def _populateList(self):
|
||||
"""Populate the list box with recent project data.
|
||||
"""
|
||||
dataList = []
|
||||
for projPath in self.mainConf.recentProj:
|
||||
theEntry = self.mainConf.recentProj[projPath]
|
||||
theTitle = theEntry.get("title", "")
|
||||
theTime = theEntry.get("time", 0)
|
||||
theWords = theEntry.get("words", 0)
|
||||
dataList.append([theTitle, theTime, theWords, projPath])
|
||||
|
||||
self.listBox.clear()
|
||||
sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
|
||||
for theTitle, theTime, theWords, projPath in sortList:
|
||||
dataList = self.mainConf.recentProjects.listEntries()
|
||||
sortList = sorted(dataList, key=lambda x: x[3], reverse=True)
|
||||
nwxIcon = self.mainGui.mainTheme.getIcon("proj_nwx")
|
||||
for path, title, words, time in sortList:
|
||||
newItem = QTreeWidgetItem([""]*4)
|
||||
newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx"))
|
||||
newItem.setText(self.C_NAME, theTitle)
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, projPath)
|
||||
newItem.setText(self.C_COUNT, formatInt(theWords))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X"))
|
||||
newItem.setIcon(self.C_NAME, nwxIcon)
|
||||
newItem.setText(self.C_NAME, title)
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, path)
|
||||
newItem.setText(self.C_COUNT, formatInt(words))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X"))
|
||||
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
|
||||
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
|
||||
@@ -294,7 +283,7 @@ class GuiProjectLoad(QDialog):
|
||||
if self.listBox.topLevelItemCount() > 0:
|
||||
self.listBox.topLevelItem(0).setSelected(True)
|
||||
|
||||
projColWidth = self.mainConf.getProjColWidths()
|
||||
projColWidth = self.mainConf.projLoadColWidths
|
||||
if len(projColWidth) == 3:
|
||||
self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
|
||||
self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
|
||||
|
||||
@@ -36,15 +36,20 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.common import simplified
|
||||
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
|
||||
from novelwriter.custom import QSwitch, PagedDialog, QConfigLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectSettings(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
TAB_MAIN = 0
|
||||
TAB_STATUS = 1
|
||||
TAB_IMPORT = 2
|
||||
TAB_REPLACE = 3
|
||||
|
||||
def __init__(self, mainGui, focusTab=TAB_MAIN):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectSettings ...")
|
||||
self.setObjectName("GuiProjectSettings")
|
||||
@@ -67,10 +72,10 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject)
|
||||
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True)
|
||||
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject)
|
||||
self.tabMain = GuiProjectEditMain(self)
|
||||
self.tabStatus = GuiProjectEditStatus(self, True)
|
||||
self.tabImport = GuiProjectEditStatus(self, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self)
|
||||
|
||||
self.addTab(self.tabMain, self.tr("Settings"))
|
||||
self.addTab(self.tabStatus, self.tr("Status"))
|
||||
@@ -83,12 +88,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
# Flags
|
||||
self.spellChanged = False
|
||||
self._spellChanged = False
|
||||
|
||||
# Focus Tab
|
||||
self._focusTab(focusTab)
|
||||
|
||||
logger.debug("GuiProjectSettings initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def spellChanged(self):
|
||||
return self._spellChanged
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -96,21 +108,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
def _doSave(self):
|
||||
"""Save settings and close dialog.
|
||||
"""
|
||||
logger.verbose("GuiProjectSettings save button clicked")
|
||||
|
||||
projName = self.tabMain.editName.text()
|
||||
bookTitle = self.tabMain.editTitle.text()
|
||||
bookAuthors = self.tabMain.editAuthors.toPlainText()
|
||||
spellLang = self.tabMain.spellLang.currentData()
|
||||
doBackup = not self.tabMain.doBackup.isChecked()
|
||||
|
||||
self.theProject.setProjectName(projName)
|
||||
self.theProject.setBookTitle(bookTitle)
|
||||
self.theProject.setBookAuthors(bookAuthors)
|
||||
self.theProject.setProjBackup(doBackup)
|
||||
self.theProject.data.setName(projName)
|
||||
self.theProject.data.setTitle(bookTitle)
|
||||
self.theProject.data.setAuthors(bookAuthors)
|
||||
self.theProject.data.setDoBackup(doBackup)
|
||||
|
||||
# Remember this as updating spell dictionary can be expensive
|
||||
self.spellChanged = self.theProject.setSpellLang(spellLang)
|
||||
self._spellChanged = self.theProject.data.setSpellLang(spellLang)
|
||||
|
||||
if self.tabStatus.colChanged:
|
||||
newList, delList = self.tabStatus.getNewList()
|
||||
@@ -125,7 +135,7 @@ class GuiProjectSettings(PagedDialog):
|
||||
|
||||
if self.tabReplace.arChanged:
|
||||
newList = self.tabReplace.getNewList()
|
||||
self.theProject.setAutoReplace(newList)
|
||||
self.theProject.data.setAutoReplace(newList)
|
||||
|
||||
self._saveGuiSettings()
|
||||
self.accept()
|
||||
@@ -143,6 +153,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _focusTab(self, tab):
|
||||
"""Change which is the focused tab.
|
||||
"""
|
||||
if tab == self.TAB_MAIN:
|
||||
self.setCurrentWidget(self.tabMain)
|
||||
elif tab == self.TAB_STATUS:
|
||||
self.setCurrentWidget(self.tabStatus)
|
||||
elif tab == self.TAB_IMPORT:
|
||||
self.setCurrentWidget(self.tabImport)
|
||||
elif tab == self.TAB_REPLACE:
|
||||
self.setCurrentWidget(self.tabReplace)
|
||||
return
|
||||
|
||||
def _saveGuiSettings(self):
|
||||
"""Save GUI settings.
|
||||
"""
|
||||
@@ -166,12 +189,12 @@ class GuiProjectSettings(PagedDialog):
|
||||
|
||||
class GuiProjectEditMain(QWidget):
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = theProject
|
||||
self.mainGui = projGui.mainGui
|
||||
self.theProject = projGui.theProject
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -186,7 +209,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(200)
|
||||
self.editName.setMaximumWidth(xW)
|
||||
self.editName.setText(self.theProject.projName)
|
||||
self.editName.setText(self.theProject.data.name)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Project name"),
|
||||
self.editName,
|
||||
@@ -196,7 +219,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editTitle = QLineEdit()
|
||||
self.editTitle.setMaxLength(200)
|
||||
self.editTitle.setMaximumWidth(xW)
|
||||
self.editTitle.setText(self.theProject.bookTitle)
|
||||
self.editTitle.setText(self.theProject.data.title)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Novel title"),
|
||||
self.editTitle,
|
||||
@@ -206,7 +229,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editAuthors = QPlainTextEdit()
|
||||
self.editAuthors.setMaximumHeight(xH)
|
||||
self.editAuthors.setMaximumWidth(xW)
|
||||
self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors))
|
||||
self.editAuthors.setPlainText("\n".join(self.theProject.data.authors))
|
||||
self.mainForm.addRow(
|
||||
self.tr("Author(s)"),
|
||||
self.editAuthors,
|
||||
@@ -229,13 +252,13 @@ class GuiProjectEditMain(QWidget):
|
||||
)
|
||||
|
||||
spellIdx = 0
|
||||
if self.theProject.projSpell is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.projSpell)
|
||||
if self.theProject.data.spellLang is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.data.spellLang)
|
||||
if spellIdx != -1:
|
||||
self.spellLang.setCurrentIndex(spellIdx)
|
||||
|
||||
self.doBackup = QSwitch(self)
|
||||
self.doBackup.setChecked(not self.theProject.doBackup)
|
||||
self.doBackup.setChecked(not self.theProject.data.doBackup)
|
||||
self.mainForm.addRow(
|
||||
self.tr("No backup on close"),
|
||||
self.doBackup,
|
||||
@@ -256,20 +279,20 @@ class GuiProjectEditStatus(QWidget):
|
||||
COL_ROLE = Qt.UserRole + 1
|
||||
NUM_ROLE = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, mainGui, theProject, isStatus):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui, isStatus):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = theProject
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = projGui.mainGui
|
||||
self.theProject = projGui.theProject
|
||||
self.mainTheme = projGui.mainGui.mainTheme
|
||||
|
||||
if isStatus:
|
||||
self.theStatus = self.theProject.statusItems
|
||||
self.theStatus = self.theProject.data.itemStatus
|
||||
pageLabel = self.tr("Novel File Status Levels")
|
||||
colSetting = "statusColW"
|
||||
else:
|
||||
self.theStatus = self.theProject.importItems
|
||||
self.theStatus = self.theProject.data.itemImport
|
||||
pageLabel = self.tr("Note File Importance Levels")
|
||||
colSetting = "importColW"
|
||||
|
||||
@@ -367,11 +390,12 @@ class GuiProjectEditStatus(QWidget):
|
||||
newList = []
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
item = self.listBox.topLevelItem(n)
|
||||
newList.append({
|
||||
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
||||
"name": item.text(self.COL_LABEL),
|
||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||
})
|
||||
if item is not None:
|
||||
newList.append({
|
||||
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
||||
"name": item.text(self.COL_LABEL),
|
||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||
})
|
||||
return newList, self.colDeleted
|
||||
|
||||
return [], []
|
||||
@@ -470,7 +494,8 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.listBox.insertTopLevelItem(nIndex, cItem)
|
||||
self.listBox.clearSelection()
|
||||
|
||||
cItem.setSelected(True)
|
||||
if cItem is not None:
|
||||
cItem.setSelected(True)
|
||||
self.colChanged = True
|
||||
|
||||
return
|
||||
@@ -527,13 +552,13 @@ class GuiProjectEditReplace(QWidget):
|
||||
COL_KEY = 0
|
||||
COL_REPL = 1
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = theProject
|
||||
self.mainGui = projGui.mainGui
|
||||
self.mainTheme = projGui.mainGui.mainTheme
|
||||
self.theProject = projGui.theProject
|
||||
self.arChanged = False
|
||||
|
||||
wCol0 = self.mainConf.pxInt(
|
||||
@@ -553,7 +578,7 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
||||
self.listBox.setIndentation(0)
|
||||
|
||||
for aKey, aVal in self.theProject.autoReplace.items():
|
||||
for aKey, aVal in self.theProject.data.autoReplace.items():
|
||||
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
|
||||
@@ -619,10 +644,11 @@ class GuiProjectEditReplace(QWidget):
|
||||
newList = {}
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
tItem = self.listBox.topLevelItem(n)
|
||||
aKey = self._stripNotAllowed(tItem.text(0))
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
if tItem is not None:
|
||||
aKey = self._stripNotAllowed(tItem.text(0))
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
|
||||
return newList
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
|
||||
selectedQuote = ""
|
||||
|
||||
def __init__(self, parent=None, currentQuote='"'):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiUpdates(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiUpdates ...")
|
||||
self.setObjectName("GuiUpdates")
|
||||
@@ -135,7 +135,7 @@ class GuiUpdates(QDialog):
|
||||
logException()
|
||||
|
||||
relVersion = rawData.get("tag_name", "Unknown")
|
||||
relDate = rawData.get("created_at", None)
|
||||
relDate = rawData.get("created_at", "")
|
||||
|
||||
try:
|
||||
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
|
||||
|
||||
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
|
||||
@@ -43,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiWordList(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiWordList ...")
|
||||
self.setObjectName("GuiWordList")
|
||||
@@ -150,13 +151,19 @@ class GuiWordList(QDialog):
|
||||
"""
|
||||
self._saveGuiSettings()
|
||||
|
||||
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
||||
tmpFile = dctFile + "~"
|
||||
dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
|
||||
if not isinstance(dctFile, Path):
|
||||
return False
|
||||
|
||||
tmpFile = dctFile.with_suffix(".tmp")
|
||||
try:
|
||||
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
|
||||
for i in range(self.listBox.count()):
|
||||
outFile.write(self.listBox.item(i).text() + "\n")
|
||||
item = self.listBox.item(i)
|
||||
if item is not None:
|
||||
outFile.write(item.text() + "\n")
|
||||
|
||||
tmpFile.replace(dctFile)
|
||||
|
||||
except Exception:
|
||||
logger.error("Could not save new word list")
|
||||
@@ -164,9 +171,6 @@ class GuiWordList(QDialog):
|
||||
self.reject()
|
||||
return False
|
||||
|
||||
if os.path.isfile(dctFile):
|
||||
os.unlink(dctFile)
|
||||
os.rename(tmpFile, dctFile)
|
||||
self.accept()
|
||||
|
||||
return True
|
||||
@@ -185,10 +189,12 @@ class GuiWordList(QDialog):
|
||||
def _loadWordList(self):
|
||||
"""Load the project's word list, if it exists.
|
||||
"""
|
||||
self.listBox.clear()
|
||||
wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
|
||||
if not isinstance(wordList, Path):
|
||||
return False
|
||||
|
||||
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
||||
if not os.path.isfile(wordList):
|
||||
self.listBox.clear()
|
||||
if not wordList.exists():
|
||||
logger.debug("No project dictionary file found")
|
||||
return False
|
||||
|
||||
|
||||
@@ -112,9 +112,10 @@ class nwDocInsert(Enum):
|
||||
QUOTE_RS = 2
|
||||
QUOTE_LD = 3
|
||||
QUOTE_RD = 4
|
||||
NEW_PAGE = 5
|
||||
VSPACE_S = 6
|
||||
VSPACE_M = 7
|
||||
SYNOPSIS = 5
|
||||
NEW_PAGE = 6
|
||||
VSPACE_S = 7
|
||||
VSPACE_M = 8
|
||||
|
||||
# END Enum nwDocInsert
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ def logException():
|
||||
"""Log the content of an exception message.
|
||||
"""
|
||||
exType, exValue, _ = sys.exc_info()
|
||||
logger.error("%s: %s", exType.__name__, str(exValue))
|
||||
if exType is not None:
|
||||
logger.error("%s: %s", exType.__name__, str(exValue))
|
||||
|
||||
|
||||
def formatException(exc):
|
||||
@@ -61,7 +62,7 @@ def formatException(exc):
|
||||
class NWErrorMessage(QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("NWErrorMessage")
|
||||
|
||||
# Widgets
|
||||
@@ -131,7 +132,7 @@ class NWErrorMessage(QDialog):
|
||||
|
||||
try:
|
||||
import lxml
|
||||
lxmlVersion = lxml.__version__
|
||||
lxmlVersion = lxml.__version__ # type: ignore
|
||||
except Exception:
|
||||
lxmlVersion = "Unknown"
|
||||
|
||||
@@ -198,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace):
|
||||
|
||||
try:
|
||||
# Try a controlled shutdown
|
||||
nwGUI.closeProject(isYes=True)
|
||||
nwGUI.closeMain()
|
||||
nwGUI.closeProject(isYes=True) # type: ignore
|
||||
nwGUI.closeMain() # type: ignore
|
||||
logger.info("Emergency shutdown successful")
|
||||
|
||||
except Exception as exc:
|
||||
|
||||
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
BLOCK_TITLE = 4
|
||||
|
||||
def __init__(self, theDoc, mainGui, spEnchant):
|
||||
QSyntaxHighlighter.__init__(self, theDoc)
|
||||
super().__init__(theDoc)
|
||||
|
||||
logger.debug("Initialising GuiDocHighlighter ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -151,11 +151,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
# Quoted Strings
|
||||
if self.mainConf.highlightQuotes:
|
||||
fmtDbl = self.mainConf.fmtDoubleQuotes
|
||||
fmtSng = self.mainConf.fmtSingleQuotes
|
||||
fmtDblO = self.mainConf.fmtDQuoteOpen
|
||||
fmtDblC = self.mainConf.fmtDQuoteClose
|
||||
fmtSngO = self.mainConf.fmtSQuoteOpen
|
||||
fmtSngC = self.mainConf.fmtSQuoteClose
|
||||
|
||||
# Straight Quotes
|
||||
if fmtDbl != ["\"", "\""]:
|
||||
if not (fmtDblO == fmtDblC == "\""):
|
||||
self.hRules.append((
|
||||
"(\\B\")(.*?)(\"\\B)", {
|
||||
0: self.hStyles["dialogue1"],
|
||||
@@ -165,7 +167,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# Double Quotes
|
||||
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
|
||||
f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", {
|
||||
0: self.hStyles["dialogue2"],
|
||||
}
|
||||
))
|
||||
@@ -173,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# Single Quotes
|
||||
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
|
||||
f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", {
|
||||
0: self.hStyles["dialogue3"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QTextBrowser.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocViewer ...")
|
||||
|
||||
@@ -102,6 +102,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
return True
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.docHeader.updateTheme()
|
||||
self.docFooter.updateTheme()
|
||||
return
|
||||
|
||||
def initViewer(self):
|
||||
"""Set editor settings from main config.
|
||||
"""
|
||||
@@ -150,10 +157,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
if self._docHandle is not None:
|
||||
@@ -193,10 +197,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
return False
|
||||
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
|
||||
# Must be before setHtml
|
||||
if updateHistory:
|
||||
@@ -216,7 +217,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.verticalScrollBar().setValue(sPos)
|
||||
|
||||
self._docHandle = tHandle
|
||||
self.theProject.setLastViewed(tHandle)
|
||||
self.theProject._data.setLastHandle(tHandle, "viewer")
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.updateDocMargins()
|
||||
|
||||
@@ -247,7 +248,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Wrapper function for various document actions on the current
|
||||
document.
|
||||
"""
|
||||
logger.verbose("Requesting action: '%s'", theAction.name)
|
||||
logger.debug("Requesting action: '%s'", theAction.name)
|
||||
if self._docHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
@@ -270,7 +271,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
if not isinstance(tAnchor, str):
|
||||
return False
|
||||
if tAnchor.startswith("#"):
|
||||
logger.verbose("Moving to anchor '%s'", tAnchor)
|
||||
logger.debug("Moving to anchor '%s'", tAnchor)
|
||||
self.setSource(QUrl(tAnchor))
|
||||
return True
|
||||
|
||||
@@ -356,7 +357,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
logger.verbose("Cursor moved to line %d", theLine)
|
||||
logger.debug("Cursor moved to line %d", theLine)
|
||||
return True
|
||||
|
||||
def setScrollPosition(self, thePos):
|
||||
@@ -402,7 +403,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Process a clicked link internally in the document.
|
||||
"""
|
||||
theLink = theURL.url()
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
logger.debug("Clicked link: '%s'", theLink)
|
||||
if len(theLink) > 0:
|
||||
theBits = theLink.split("=")
|
||||
if len(theBits) == 2:
|
||||
@@ -461,7 +462,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
has its margins adjusted according to user preferences.
|
||||
"""
|
||||
self.updateDocMargins()
|
||||
QTextBrowser.resizeEvent(self, theEvent)
|
||||
super().resizeEvent(theEvent)
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, theEvent):
|
||||
@@ -472,7 +473,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
elif theEvent.button() == Qt.ForwardButton:
|
||||
self.navForward()
|
||||
else:
|
||||
QTextBrowser.mouseReleaseEvent(self, theEvent)
|
||||
super().mouseReleaseEvent(theEvent)
|
||||
return
|
||||
|
||||
##
|
||||
@@ -568,7 +569,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
# END Class GuiDocViewer
|
||||
|
||||
|
||||
class GuiDocViewHistory():
|
||||
class GuiDocViewHistory:
|
||||
|
||||
def __init__(self, docViewer):
|
||||
|
||||
@@ -584,7 +585,7 @@ class GuiDocViewHistory():
|
||||
def clear(self):
|
||||
"""Clear the view history.
|
||||
"""
|
||||
logger.verbose("View history cleared")
|
||||
logger.debug("View history cleared")
|
||||
self._navHistory = []
|
||||
self._posHistory = []
|
||||
self._currPos = -1
|
||||
@@ -598,7 +599,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
if self._currPos >= 0 and self._currPos < len(self._navHistory):
|
||||
if tHandle == self._navHistory[self._currPos]:
|
||||
logger.verbose("Not updating view hsitory")
|
||||
logger.debug("Not updating view hsitory")
|
||||
return False
|
||||
|
||||
self._truncateHistory(self._currPos)
|
||||
@@ -613,7 +614,7 @@ class GuiDocViewHistory():
|
||||
|
||||
self._dumpHistory()
|
||||
|
||||
logger.verbose("Added '%s' to view history", tHandle)
|
||||
logger.debug("Added '%s' to view history", tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -622,7 +623,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
newPos = self._currPos + 1
|
||||
if newPos < len(self._navHistory):
|
||||
logger.verbose("Move forward in view history")
|
||||
logger.debug("Move forward in view history")
|
||||
self._prevPos = self._currPos
|
||||
self._updateScrollBar()
|
||||
|
||||
@@ -640,7 +641,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
newPos = self._currPos - 1
|
||||
if newPos >= 0:
|
||||
logger.verbose("Move backward in view history")
|
||||
logger.debug("Move backward in view history")
|
||||
self._prevPos = self._currPos
|
||||
self._updateScrollBar()
|
||||
|
||||
@@ -686,11 +687,11 @@ class GuiDocViewHistory():
|
||||
|
||||
def _dumpHistory(self):
|
||||
"""Debug function to dump history to the logger. Since it is a
|
||||
for loop, it is skipped entirely if log level isn't VERBOSE.
|
||||
for loop, it is skipped entirely if log level isn't DEBUG.
|
||||
"""
|
||||
if logger.getEffectiveLevel() < logging.DEBUG:
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
|
||||
logger.verbose(
|
||||
logger.debug(
|
||||
"History %02d: %s %13s [x:%d]" % (
|
||||
i + 1, ">" if i == self._currPos else " ", h, p
|
||||
)
|
||||
@@ -708,7 +709,7 @@ class GuiDocViewHistory():
|
||||
class GuiDocViewHeader(QWidget):
|
||||
|
||||
def __init__(self, docViewer):
|
||||
QWidget.__init__(self, docViewer)
|
||||
super().__init__(parent=docViewer)
|
||||
|
||||
logger.debug("Initialising GuiDocViewHeader ...")
|
||||
|
||||
@@ -741,51 +742,38 @@ class GuiDocViewHeader(QWidget):
|
||||
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
|
||||
self.theTitle.setFont(lblFont)
|
||||
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*self.mainTheme.colText)
|
||||
|
||||
# Buttons
|
||||
self.backButton = QToolButton(self)
|
||||
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
|
||||
self.backButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.backButton.setIconSize(QSize(fPx, fPx))
|
||||
self.backButton.setFixedSize(fPx, fPx)
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.backButton.setVisible(False)
|
||||
self.backButton.setToolTip(self.tr("Go backward"))
|
||||
self.backButton.clicked.connect(self.docViewer.navBackward)
|
||||
|
||||
self.forwardButton = QToolButton(self)
|
||||
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
|
||||
self.forwardButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.forwardButton.setIconSize(QSize(fPx, fPx))
|
||||
self.forwardButton.setFixedSize(fPx, fPx)
|
||||
self.forwardButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.forwardButton.setVisible(False)
|
||||
self.forwardButton.setToolTip(self.tr("Go forward"))
|
||||
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
||||
|
||||
self.refreshButton = QToolButton(self)
|
||||
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.refreshButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.refreshButton.setIconSize(QSize(fPx, fPx))
|
||||
self.refreshButton.setFixedSize(fPx, fPx)
|
||||
self.refreshButton.setStyleSheet(buttonStyle)
|
||||
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.refreshButton.setVisible(False)
|
||||
self.refreshButton.setToolTip(self.tr("Reload the document"))
|
||||
self.refreshButton.clicked.connect(self._refreshDocument)
|
||||
|
||||
self.closeButton = QToolButton(self)
|
||||
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
|
||||
self.closeButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.closeButton.setIconSize(QSize(fPx, fPx))
|
||||
self.closeButton.setFixedSize(fPx, fPx)
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.closeButton.setVisible(False)
|
||||
self.closeButton.setToolTip(self.tr("Close the document"))
|
||||
@@ -809,7 +797,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.setMinimumHeight(fPx + 2*cM)
|
||||
|
||||
# Fix the Colours
|
||||
self.matchColours()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiDocViewHeader initialisation complete")
|
||||
|
||||
@@ -819,6 +807,28 @@ class GuiDocViewHeader(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
|
||||
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
|
||||
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
|
||||
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*self.mainTheme.colText)
|
||||
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.setStyleSheet(buttonStyle)
|
||||
self.refreshButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
|
||||
self.matchColours()
|
||||
|
||||
return
|
||||
|
||||
def matchColours(self):
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -879,12 +889,14 @@ class GuiDocViewHeader(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _closeDocument(self):
|
||||
"""Trigger the close editor/viewer on the main window.
|
||||
"""
|
||||
self.mainGui.closeDocViewer()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _refreshDocument(self):
|
||||
"""Reload the content of the document.
|
||||
"""
|
||||
@@ -915,7 +927,7 @@ class GuiDocViewHeader(QWidget):
|
||||
class GuiDocViewFooter(QWidget):
|
||||
|
||||
def __init__(self, docViewer):
|
||||
QWidget.__init__(self, docViewer)
|
||||
super().__init__(parent=docViewer)
|
||||
|
||||
logger.debug("Initialising GuiDocViewFooter ...")
|
||||
|
||||
@@ -932,33 +944,13 @@ class GuiDocViewFooter(QWidget):
|
||||
bSp = self.mainConf.pxInt(2)
|
||||
hSp = self.mainConf.pxInt(8)
|
||||
|
||||
# Icons
|
||||
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
|
||||
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
|
||||
stickyIcon = QIcon()
|
||||
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
|
||||
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
|
||||
|
||||
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
|
||||
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
|
||||
bulletIcon = QIcon()
|
||||
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
|
||||
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
|
||||
|
||||
# Main Widget Settings
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*self.mainTheme.colText)
|
||||
|
||||
# Show/Hide Details
|
||||
self.showHide = QToolButton(self)
|
||||
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showHide.setStyleSheet(buttonStyle)
|
||||
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
|
||||
self.showHide.setIconSize(QSize(fPx, fPx))
|
||||
self.showHide.setFixedSize(QSize(fPx, fPx))
|
||||
self.showHide.clicked.connect(self._doShowHide)
|
||||
@@ -968,8 +960,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.stickyRefs = QToolButton(self)
|
||||
self.stickyRefs.setCheckable(True)
|
||||
self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.stickyRefs.setStyleSheet(buttonStyle)
|
||||
self.stickyRefs.setIcon(stickyIcon)
|
||||
self.stickyRefs.setIconSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.toggled.connect(self._doToggleSticky)
|
||||
@@ -982,8 +972,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showComments.setCheckable(True)
|
||||
self.showComments.setChecked(self.mainConf.viewComments)
|
||||
self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showComments.setStyleSheet(buttonStyle)
|
||||
self.showComments.setIcon(bulletIcon)
|
||||
self.showComments.setIconSize(QSize(fPx, fPx))
|
||||
self.showComments.setFixedSize(QSize(fPx, fPx))
|
||||
self.showComments.toggled.connect(self._doToggleComments)
|
||||
@@ -994,8 +982,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showSynopsis.setCheckable(True)
|
||||
self.showSynopsis.setChecked(self.mainConf.viewSynopsis)
|
||||
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showSynopsis.setStyleSheet(buttonStyle)
|
||||
self.showSynopsis.setIcon(bulletIcon)
|
||||
self.showSynopsis.setIconSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
||||
@@ -1069,7 +1055,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.setMinimumHeight(fPx + 2*cM)
|
||||
|
||||
# Fix the Colours
|
||||
self.matchColours()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiDocViewFooter initialisation complete")
|
||||
|
||||
@@ -1079,6 +1065,46 @@ class GuiDocViewFooter(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
# Icons
|
||||
|
||||
fPx = int(0.9*self.mainTheme.fontPixelSize)
|
||||
|
||||
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
|
||||
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
|
||||
stickyIcon = QIcon()
|
||||
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
|
||||
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
|
||||
|
||||
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
|
||||
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
|
||||
bulletIcon = QIcon()
|
||||
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
|
||||
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
|
||||
|
||||
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
|
||||
self.stickyRefs.setIcon(stickyIcon)
|
||||
self.showComments.setIcon(bulletIcon)
|
||||
self.showSynopsis.setIcon(bulletIcon)
|
||||
|
||||
# StyleSheets
|
||||
|
||||
buttonStyle = (
|
||||
"QToolButton {{border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||
).format(*self.mainTheme.colText)
|
||||
|
||||
self.showHide.setStyleSheet(buttonStyle)
|
||||
self.stickyRefs.setStyleSheet(buttonStyle)
|
||||
self.showComments.setStyleSheet(buttonStyle)
|
||||
self.showSynopsis.setStyleSheet(buttonStyle)
|
||||
|
||||
self.matchColours()
|
||||
|
||||
return
|
||||
|
||||
def matchColours(self):
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -1100,6 +1126,7 @@ class GuiDocViewFooter(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _doShowHide(self):
|
||||
"""Toggle the expand/collapse of the panel.
|
||||
"""
|
||||
@@ -1107,26 +1134,29 @@ class GuiDocViewFooter(QWidget):
|
||||
self.viewMeta.setVisible(not isVisible)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleSticky(self, theState):
|
||||
"""Toggle the sticky flag for the reference panel.
|
||||
"""
|
||||
logger.verbose("Reference sticky is %s", str(theState))
|
||||
logger.debug("Reference sticky is %s", str(theState))
|
||||
self.docViewer.stickyRef = theState
|
||||
if not theState and self.docViewer.docHandle() is not None:
|
||||
self.viewMeta.refreshReferences(self.docViewer.docHandle())
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleComments(self, theState):
|
||||
"""Toggle the view comment button and reload the document.
|
||||
"""
|
||||
self.mainConf.setViewComments(theState)
|
||||
self.mainConf.viewComments = theState
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleSynopsis(self, theState):
|
||||
"""Toggle the view synopsis button and reload the document.
|
||||
"""
|
||||
self.mainConf.setViewSynopsis(theState)
|
||||
self.mainConf.viewSynopsis = theState
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@@ -1141,7 +1171,7 @@ class GuiDocViewFooter(QWidget):
|
||||
class GuiDocViewDetails(QScrollArea):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QScrollArea.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocViewDetails ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -1205,7 +1235,7 @@ class GuiDocViewDetails(QScrollArea):
|
||||
"""Capture the link-click and forward it to the document viewer
|
||||
class for handling.
|
||||
"""
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
logger.debug("Clicked link: '%s'", theLink)
|
||||
if len(theLink) == 21:
|
||||
tHandle = theLink[:13]
|
||||
tAnchor = theLink[13:]
|
||||
|
||||
@@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QFont, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
|
||||
from novelwriter.enum import nwItemType
|
||||
from novelwriter.constants import trConst, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,7 +38,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiItemDetails(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiItemDetails ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -54,12 +53,8 @@ class GuiItemDetails(QWidget):
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
vSp = self.mainConf.pxInt(1)
|
||||
mPx = self.mainConf.pxInt(6)
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
fPt = self.mainTheme.fontPointSize
|
||||
|
||||
self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx))
|
||||
self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx))
|
||||
|
||||
fntLabel = QFont()
|
||||
fntLabel.setBold(True)
|
||||
fntLabel.setPointSizeF(0.9*fPt)
|
||||
@@ -180,6 +175,8 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
# Make sure the columns for flags and counts don't resize too often
|
||||
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
|
||||
countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
|
||||
@@ -220,6 +217,12 @@ class GuiItemDetails(QWidget):
|
||||
"""
|
||||
self.updateViewBox(self._itemHandle)
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.updateViewBox(self._itemHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
@@ -247,13 +250,13 @@ class GuiItemDetails(QWidget):
|
||||
if len(theLabel) > 100:
|
||||
theLabel = theLabel[:96].rstrip()+" ..."
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isExported:
|
||||
self.labelIcon.setPixmap(self._expCheck)
|
||||
if nwItem.isFileType():
|
||||
if nwItem.isActive:
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("checked", (iPx, iPx)))
|
||||
else:
|
||||
self.labelIcon.setPixmap(self._expCross)
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("unchecked", (iPx, iPx)))
|
||||
else:
|
||||
self.labelIcon.setPixmap(QPixmap(1, 1))
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("noncheckable", (iPx, iPx)))
|
||||
|
||||
self.labelData.setText(theLabel)
|
||||
|
||||
@@ -274,17 +277,16 @@ class GuiItemDetails(QWidget):
|
||||
# Layout
|
||||
# ======
|
||||
|
||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
||||
usageIcon = self.mainTheme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||
)
|
||||
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
|
||||
self.usageData.setText(nwItem.describeMe(hLevel))
|
||||
self.usageData.setText(nwItem.describeMe())
|
||||
|
||||
# Counts
|
||||
# ======
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isFileType():
|
||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
||||
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
||||
|
||||
@@ -26,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import pathname2url
|
||||
|
||||
@@ -40,9 +41,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiMainMenu(QMenuBar):
|
||||
"""The GUI main menu. All menu actions are defined here with the
|
||||
main menu as the owner. Each widget that need them elsewhere need to
|
||||
add them from this class.
|
||||
"""
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QMenuBar.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiMainMenu ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -79,12 +84,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aSpellCheck.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setFocusMode(self, theMode):
|
||||
"""Forward focus mode check state to its action.
|
||||
"""
|
||||
self.aFocusMode.setChecked(theMode)
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -106,10 +105,11 @@ class GuiMainMenu(QMenuBar):
|
||||
def _openUserManualFile(self):
|
||||
"""Open the documentation in PDF format.
|
||||
"""
|
||||
if self.mainConf.pdfDocs is None:
|
||||
return False
|
||||
QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs))))
|
||||
return True
|
||||
if isinstance(self.mainConf.pdfDocs, Path):
|
||||
QDesktopServices.openUrl(
|
||||
QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs))))
|
||||
)
|
||||
return
|
||||
|
||||
##
|
||||
# Menu Builders
|
||||
@@ -164,14 +164,14 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
# Project > Edit
|
||||
self.aEditItem = QAction(self.tr("Rename Item"), self)
|
||||
self.aEditItem.setShortcuts(["F2"])
|
||||
self.aEditItem.setShortcut("F2")
|
||||
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
|
||||
self.projMenu.addAction(self.aEditItem)
|
||||
|
||||
# Project > Delete
|
||||
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
||||
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
|
||||
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
|
||||
self.projMenu.addAction(self.aDeleteItem)
|
||||
|
||||
# Project > Empty Trash
|
||||
@@ -244,16 +244,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
||||
self.docuMenu.addAction(self.aImportFile)
|
||||
|
||||
# Document > Merge Documents
|
||||
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
|
||||
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
|
||||
self.docuMenu.addAction(self.aMergeDocs)
|
||||
|
||||
# Document > Split Document
|
||||
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
|
||||
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
|
||||
self.docuMenu.addAction(self.aSplitDoc)
|
||||
|
||||
return
|
||||
|
||||
def _buildEditMenu(self):
|
||||
@@ -375,8 +365,6 @@ class GuiMainMenu(QMenuBar):
|
||||
# View > Focus Mode
|
||||
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
|
||||
self.aFocusMode.setShortcut("F8")
|
||||
self.aFocusMode.setCheckable(True)
|
||||
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
|
||||
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
|
||||
self.viewMenu.addAction(self.aFocusMode)
|
||||
|
||||
@@ -568,6 +556,15 @@ class GuiMainMenu(QMenuBar):
|
||||
)
|
||||
self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0])
|
||||
|
||||
# Insert > Special Comments
|
||||
self.mInsComments = self.insertMenu.addMenu(self.tr("Special Comments"))
|
||||
|
||||
# Insert > Synopsis Comment
|
||||
self.aInsSynopsis = QAction(self.tr("Synopsis Comment"), self)
|
||||
self.aInsSynopsis.setShortcut("Ctrl+K, S")
|
||||
self.aInsSynopsis.triggered.connect(lambda: self._docInsert(nwDocInsert.SYNOPSIS))
|
||||
self.mInsComments.addAction(self.aInsSynopsis)
|
||||
|
||||
# Insert > Symbols
|
||||
self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space"))
|
||||
|
||||
@@ -799,7 +796,7 @@ class GuiMainMenu(QMenuBar):
|
||||
# Tools > Check Spelling
|
||||
self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
|
||||
self.aSpellCheck.setCheckable(True)
|
||||
self.aSpellCheck.setChecked(self.theProject.spellCheck)
|
||||
self.aSpellCheck.setChecked(self.theProject.data.spellCheck)
|
||||
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
|
||||
self.aSpellCheck.setShortcut("Ctrl+F7")
|
||||
self.toolsMenu.addAction(self.aSpellCheck)
|
||||
@@ -829,7 +826,7 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
# Tools > Backup
|
||||
self.aBackupProject = QAction(self.tr("Backup Project"), self)
|
||||
self.aBackupProject.triggered.connect(lambda: self.theProject.zipIt(True))
|
||||
self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(True))
|
||||
self.toolsMenu.addAction(self.aBackupProject)
|
||||
|
||||
# Tools > Export Project
|
||||
@@ -886,7 +883,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.helpMenu.addAction(self.aHelpDocs)
|
||||
|
||||
# Help > User Manual (PDF)
|
||||
if self.mainConf.pdfDocs is not None:
|
||||
if isinstance(self.mainConf.pdfDocs, Path):
|
||||
self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self)
|
||||
self.aPdfDocs.setShortcut("Shift+F1")
|
||||
self.aPdfDocs.triggered.connect(self._openUserManualFile)
|
||||
|
||||
@@ -35,11 +35,11 @@ from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||
QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget
|
||||
QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem,
|
||||
QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwDocMode, nwItemClass
|
||||
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
|
||||
|
||||
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
|
||||
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainGui = mainGui
|
||||
self.theProject = mainGui.theProject
|
||||
@@ -71,6 +71,7 @@ class GuiNovelView(QWidget):
|
||||
# Build GUI
|
||||
self.novelTree = GuiNovelTree(self)
|
||||
self.novelBar = GuiNovelToolBar(self)
|
||||
self.novelBar.setEnabled(False)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
@@ -84,6 +85,7 @@ class GuiNovelView(QWidget):
|
||||
# Function Mappings
|
||||
self.updateWordCounts = self.novelTree.updateWordCounts
|
||||
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
||||
self.setActiveHandle = self.novelTree.setActiveHandle
|
||||
|
||||
return
|
||||
|
||||
@@ -91,14 +93,24 @@ class GuiNovelView(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.novelBar.updateTheme()
|
||||
self.novelTree.updateTheme()
|
||||
self.refreshTree()
|
||||
return
|
||||
|
||||
def initSettings(self):
|
||||
"""Initialise GUI elements that depend on specific settings.
|
||||
"""
|
||||
self.novelTree.initSettings()
|
||||
return
|
||||
|
||||
def refreshTree(self):
|
||||
"""Refresh the current tree.
|
||||
"""
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel)
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
|
||||
return
|
||||
|
||||
def clearProject(self):
|
||||
@@ -106,12 +118,13 @@ class GuiNovelView(QWidget):
|
||||
"""
|
||||
self.novelTree.clearContent()
|
||||
self.novelBar.clearContent()
|
||||
self.novelBar.setEnabled(False)
|
||||
return
|
||||
|
||||
def openProjectTasks(self):
|
||||
"""Run opening project tasks.
|
||||
"""Run open project tasks.
|
||||
"""
|
||||
lastNovel = self.theProject.lastNovel
|
||||
lastNovel = self.theProject.data.getLastHandle("novelTree")
|
||||
if lastNovel not in self.theProject.tree:
|
||||
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
@@ -125,6 +138,7 @@ class GuiNovelView(QWidget):
|
||||
self.novelBar.buildNovelRootMenu()
|
||||
self.novelBar.setLastColType(lastCol, doRefresh=False)
|
||||
self.novelBar.setCurrentRoot(lastNovel)
|
||||
self.novelBar.setEnabled(True)
|
||||
|
||||
return
|
||||
|
||||
@@ -135,8 +149,8 @@ class GuiNovelView(QWidget):
|
||||
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
|
||||
return
|
||||
|
||||
def setFocus(self):
|
||||
"""Forward the set focus call to the tree widget.
|
||||
def setTreeFocus(self):
|
||||
"""Set the focus to the tree widget.
|
||||
"""
|
||||
self.novelTree.setFocus()
|
||||
return
|
||||
@@ -163,7 +177,7 @@ class GuiNovelView(QWidget):
|
||||
class GuiNovelToolBar(QWidget):
|
||||
|
||||
def __init__(self, novelView):
|
||||
QTreeWidget.__init__(self, novelView)
|
||||
super().__init__(parent=novelView)
|
||||
|
||||
logger.debug("Initialising GuiNovelToolBar ...")
|
||||
|
||||
@@ -173,21 +187,11 @@ class GuiNovelToolBar(QWidget):
|
||||
self.mainTheme = novelView.mainGui.mainTheme
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
mPx = self.mainConf.pxInt(3)
|
||||
mPx = self.mainConf.pxInt(2)
|
||||
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
qPalette = self.palette()
|
||||
qPalette.setBrush(QPalette.Window, qPalette.base())
|
||||
self.setPalette(qPalette)
|
||||
|
||||
fadeCol = qPalette.text().color()
|
||||
buttonStyle = (
|
||||
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
||||
).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
||||
|
||||
# Widget Label
|
||||
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
|
||||
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -196,9 +200,7 @@ class GuiNovelToolBar(QWidget):
|
||||
# Refresh Button
|
||||
self.tbRefresh = QToolButton(self)
|
||||
self.tbRefresh.setToolTip(self.tr("Refresh"))
|
||||
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.tbRefresh.setIconSize(QSize(iPx, iPx))
|
||||
self.tbRefresh.setStyleSheet(buttonStyle)
|
||||
self.tbRefresh.clicked.connect(self._refreshNovelTree)
|
||||
|
||||
# Novel Root Menu
|
||||
@@ -208,9 +210,7 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.tbRoot = QToolButton(self)
|
||||
self.tbRoot.setToolTip(self.tr("Novel Root"))
|
||||
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
|
||||
self.tbRoot.setIconSize(QSize(iPx, iPx))
|
||||
self.tbRoot.setStyleSheet(buttonStyle)
|
||||
self.tbRoot.setMenu(self.mRoot)
|
||||
self.tbRoot.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -227,9 +227,7 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.tbMore = QToolButton(self)
|
||||
self.tbMore.setToolTip(self.tr("More Options"))
|
||||
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
|
||||
self.tbMore.setIconSize(QSize(iPx, iPx))
|
||||
self.tbMore.setStyleSheet(buttonStyle)
|
||||
self.tbMore.setMenu(self.mMore)
|
||||
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -244,6 +242,8 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiNovelToolBar initialisation complete")
|
||||
|
||||
return
|
||||
@@ -252,6 +252,31 @@ class GuiNovelToolBar(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
# Icons
|
||||
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
|
||||
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
|
||||
|
||||
qPalette = self.palette()
|
||||
qPalette.setBrush(QPalette.Window, qPalette.base())
|
||||
self.setPalette(qPalette)
|
||||
|
||||
# StyleSheets
|
||||
fadeCol = qPalette.text().color()
|
||||
buttonStyle = (
|
||||
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
||||
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
||||
).format(self.mainConf.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
||||
|
||||
self.tbRefresh.setStyleSheet(buttonStyle)
|
||||
self.tbRoot.setStyleSheet(buttonStyle)
|
||||
self.tbMore.setStyleSheet(buttonStyle)
|
||||
|
||||
return
|
||||
|
||||
def clearContent(self):
|
||||
"""Run clearing project tasks.
|
||||
"""
|
||||
@@ -297,7 +322,7 @@ class GuiNovelToolBar(QWidget):
|
||||
def _refreshNovelTree(self):
|
||||
"""Rebuild the current tree.
|
||||
"""
|
||||
rootHandle = self.theProject.lastNovel
|
||||
rootHandle = self.theProject.data.getLastHandle("novelTree")
|
||||
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
|
||||
return
|
||||
|
||||
@@ -322,10 +347,15 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
C_WORDS = 1
|
||||
C_LAST = 2
|
||||
C_EXTRA = 2
|
||||
C_MORE = 3
|
||||
|
||||
D_HANDLE = Qt.UserRole
|
||||
D_TITLE = Qt.UserRole + 1
|
||||
D_KEY = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, novelView):
|
||||
QTreeWidget.__init__(self, novelView)
|
||||
super().__init__(parent=novelView)
|
||||
|
||||
logger.debug("Initialising GuiNovelTree ...")
|
||||
|
||||
@@ -339,6 +369,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
self._treeMap = {}
|
||||
self._lastBuild = 0
|
||||
self._lastCol = NovelTreeColumn.POV
|
||||
self._actHandle = None
|
||||
|
||||
# Cached Strings
|
||||
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||
@@ -353,9 +384,11 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
self.setUniformRowHeights(True)
|
||||
self.setAllColumnsShowFocus(True)
|
||||
self.setHeaderHidden(True)
|
||||
self.setIndentation(0)
|
||||
self.setColumnCount(3)
|
||||
self.setColumnCount(4)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
@@ -367,7 +400,8 @@ class GuiNovelTree(QTreeWidget):
|
||||
treeHeader.setMinimumSectionSize(iPx + cMg)
|
||||
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
|
||||
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
|
||||
treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents)
|
||||
treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents)
|
||||
treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents)
|
||||
|
||||
# Pre-Generate Tree Formatting
|
||||
fH1 = self.font()
|
||||
@@ -378,20 +412,15 @@ class GuiNovelTree(QTreeWidget):
|
||||
fH2.setBold(True)
|
||||
|
||||
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||||
self._pIndent = [
|
||||
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
|
||||
]
|
||||
|
||||
# Connect signals
|
||||
self.clicked.connect(self._treeItemClicked)
|
||||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
||||
|
||||
# Set custom settings
|
||||
self.initSettings()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiNovelTree initialisation complete")
|
||||
|
||||
@@ -413,6 +442,13 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -436,23 +472,23 @@ class GuiNovelTree(QTreeWidget):
|
||||
def refreshTree(self, rootHandle=None, overRide=False):
|
||||
"""Called whenever the Novel tab is activated.
|
||||
"""
|
||||
logger.verbose("Requesting refresh of the novel tree")
|
||||
logger.debug("Requesting refresh of the novel tree")
|
||||
if rootHandle is None:
|
||||
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
|
||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||
if not (treeChanged or indexChanged or overRide):
|
||||
logger.verbose("No changes have been made to the novel index")
|
||||
logger.debug("No changes have been made to the novel index")
|
||||
return
|
||||
|
||||
selItem = self.selectedItems()
|
||||
titleKey = None
|
||||
if selItem:
|
||||
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
|
||||
titleKey = selItem[0].data(self.C_TITLE, self.D_KEY)
|
||||
|
||||
self._populateTree(rootHandle)
|
||||
self.theProject.setLastNovelViewed(rootHandle)
|
||||
self.theProject.data.setLastHandle(rootHandle, "novelTree")
|
||||
|
||||
if titleKey is not None and titleKey in self._treeMap:
|
||||
self._treeMap[titleKey].setSelected(True)
|
||||
@@ -476,8 +512,9 @@ class GuiNovelTree(QTreeWidget):
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
if selItem:
|
||||
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
|
||||
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1
|
||||
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
|
||||
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
|
||||
tLine = checkInt(sTitle[1:], 1) - 1
|
||||
|
||||
return tHandle, tLine
|
||||
|
||||
@@ -487,9 +524,34 @@ class GuiNovelTree(QTreeWidget):
|
||||
if self._lastCol != colType:
|
||||
logger.debug("Changing last column to %s", colType.name)
|
||||
self._lastCol = colType
|
||||
self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN)
|
||||
self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
|
||||
if doRefresh:
|
||||
self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True)
|
||||
lastNovel = self.theProject.data.getLastHandle("novelTree")
|
||||
self.refreshTree(rootHandle=lastNovel, overRide=True)
|
||||
return
|
||||
|
||||
def setActiveHandle(self, tHandle):
|
||||
"""Highlight the rows associated with a given handle.
|
||||
"""
|
||||
tStart = time()
|
||||
|
||||
self._actHandle = tHandle
|
||||
for i in range(self.topLevelItemCount()):
|
||||
tItem = self.topLevelItem(i)
|
||||
if tItem is not None:
|
||||
if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle:
|
||||
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
|
||||
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
|
||||
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
|
||||
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
|
||||
else:
|
||||
tItem.setBackground(self.C_TITLE, self.palette().base())
|
||||
tItem.setBackground(self.C_WORDS, self.palette().base())
|
||||
tItem.setBackground(self.C_EXTRA, self.palette().base())
|
||||
tItem.setBackground(self.C_MORE, self.palette().base())
|
||||
|
||||
logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -501,7 +563,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
mouse in a blank area of the tree view, and to load a document
|
||||
for viewing if the user middle-clicked.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
super().mousePressEvent(theEvent)
|
||||
|
||||
if theEvent.button() == Qt.LeftButton:
|
||||
selItem = self.indexAt(theEvent.pos())
|
||||
@@ -521,10 +583,28 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def focusOutEvent(self, theEvent):
|
||||
"""Clear the selection when the tree no longer has focus.
|
||||
"""
|
||||
super().focusOutEvent(theEvent)
|
||||
self.clearSelection()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@pyqtSlot("QModelIndex")
|
||||
def _treeItemClicked(self, mIndex):
|
||||
"""The user clicked on an item in the tree.
|
||||
"""
|
||||
if mIndex.column() == self.C_MORE:
|
||||
tHandle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_HANDLE)
|
||||
sTitle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_TITLE)
|
||||
tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight())
|
||||
self._popMetaBox(tipPos, tHandle, sTitle)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeSelectionChange(self):
|
||||
"""Extract the handle and line number of the currently selected
|
||||
@@ -554,7 +634,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
"""
|
||||
self.clearContent()
|
||||
tStart = time()
|
||||
logger.verbose("Building novel tree for root item '%s'", rootHandle)
|
||||
logger.debug("Building novel tree for root item '%s'", rootHandle)
|
||||
|
||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||
for tKey, tHandle, sTitle, novIdx in novStruct:
|
||||
@@ -563,25 +643,31 @@ class GuiNovelTree(QTreeWidget):
|
||||
if iLevel == 0:
|
||||
continue
|
||||
|
||||
newItem = QTreeWidgetItem()
|
||||
theData = (tHandle, sTitle[1:].lstrip("0"), tKey)
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel])
|
||||
newItem = QTreeWidgetItem()
|
||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||
newItem.setText(self.C_TITLE, novIdx.title)
|
||||
newItem.setData(self.C_TITLE, Qt.UserRole, theData)
|
||||
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
|
||||
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
|
||||
newItem.setData(self.C_TITLE, self.D_KEY, tKey)
|
||||
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
|
||||
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
|
||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
|
||||
|
||||
# Custom column
|
||||
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
||||
newItem.setText(self.C_LAST, lastText)
|
||||
newItem.setText(self.C_EXTRA, lastText)
|
||||
if lastText:
|
||||
newItem.setToolTip(self.C_LAST, toolTip)
|
||||
newItem.setToolTip(self.C_EXTRA, toolTip)
|
||||
|
||||
self._treeMap[tKey] = newItem
|
||||
self.addTopLevelItem(newItem)
|
||||
|
||||
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000)
|
||||
self.setActiveHandle(self._actHandle)
|
||||
|
||||
logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
|
||||
self._lastBuild = time()
|
||||
|
||||
return
|
||||
@@ -607,4 +693,49 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return "", ""
|
||||
|
||||
def _popMetaBox(self, qPos, tHandle, sTitle):
|
||||
"""Show the novel meta data box.
|
||||
"""
|
||||
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
||||
|
||||
pIndex = self.theProject.index
|
||||
novIdx = pIndex.getNovelData(tHandle, sTitle)
|
||||
refTags = pIndex.getReferences(tHandle, sTitle)
|
||||
|
||||
synopText = novIdx.synopsis
|
||||
if synopText:
|
||||
synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
|
||||
synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
|
||||
|
||||
refLines = []
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
|
||||
refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
|
||||
|
||||
refText = ""
|
||||
if refLines:
|
||||
refList = "<br>".join(refLines)
|
||||
refText = f"<p>{refList}</p>"
|
||||
|
||||
ttText = refText + synopText or self.tr("No meta data")
|
||||
if ttText:
|
||||
QToolTip.showText(qPos, ttText)
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _appendMetaTag(refs, key, lines):
|
||||
"""Generate a reference list for a given reference key.
|
||||
"""
|
||||
tags = ", ".join(refs.get(key, []))
|
||||
if tags:
|
||||
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
|
||||
return lines
|
||||
|
||||
# END Class GuiNovelTree
|
||||
|
||||
@@ -37,16 +37,16 @@ from PyQt5.QtCore import (
|
||||
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
|
||||
QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox,
|
||||
QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
|
||||
QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import (
|
||||
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||
)
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.constants import trConst, nwKeyWords, nwLabels
|
||||
from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,20 +57,22 @@ class GuiOutlineView(QWidget):
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = mainGui.theProject
|
||||
|
||||
# Build GUI
|
||||
self.outlineBar = GuiOutlineToolBar(self)
|
||||
self.outlineTree = GuiOutlineTree(self)
|
||||
self.outlineData = GuiOutlineDetails(self)
|
||||
self.outlineBar = GuiOutlineToolBar(self)
|
||||
self.outlineBar.setEnabled(False)
|
||||
|
||||
self.splitOutline = QSplitter(Qt.Vertical)
|
||||
self.splitOutline.addWidget(self.outlineTree)
|
||||
self.splitOutline.addWidget(self.outlineData)
|
||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
||||
self.splitOutline.setSizes(self.mainConf.outlinePanePos)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
@@ -96,33 +98,67 @@ class GuiOutlineView(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def splitSizes(self):
|
||||
return self.splitOutline.sizes()
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.outlineBar.updateTheme()
|
||||
self.refreshTree()
|
||||
return
|
||||
|
||||
def clearOutline(self):
|
||||
def initSettings(self):
|
||||
"""Initialise GUI elements that depend on specific settings.
|
||||
"""
|
||||
self.outlineTree.initSettings()
|
||||
self.outlineData.initSettings()
|
||||
return
|
||||
|
||||
def refreshTree(self):
|
||||
"""Refresh the current tree.
|
||||
"""
|
||||
self.outlineTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("outline"))
|
||||
return
|
||||
|
||||
def clearProject(self):
|
||||
"""Clear project-related GUI content.
|
||||
"""
|
||||
self.outlineData.clearDetails()
|
||||
self.outlineBar.setEnabled(False)
|
||||
return
|
||||
|
||||
def initOutline(self):
|
||||
self.outlineTree.initOutline()
|
||||
self.outlineData.initDetails()
|
||||
def openProjectTasks(self):
|
||||
"""Run open project tasks.
|
||||
"""
|
||||
lastOutline = self.theProject.data.getLastHandle("outline")
|
||||
if not (lastOutline in self.theProject.tree or lastOutline is None):
|
||||
lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
logger.debug("Setting outline tree to root item '%s'", lastOutline)
|
||||
|
||||
self.clearProject()
|
||||
self.outlineBar.populateNovelList()
|
||||
self.outlineBar.setCurrentRoot(lastOutline)
|
||||
self.outlineBar.setEnabled(True)
|
||||
|
||||
return
|
||||
|
||||
def closeOutline(self):
|
||||
self.outlineTree.closeOutline()
|
||||
def closeProjectTasks(self):
|
||||
self.outlineTree.closeProjectTasks()
|
||||
self.outlineData.updateClasses()
|
||||
return
|
||||
|
||||
def refreshView(self, overRide=False, novelChanged=False):
|
||||
self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged)
|
||||
return
|
||||
|
||||
def treeHasFocus(self):
|
||||
return self.outlineTree.hasFocus()
|
||||
def splitSizes(self):
|
||||
return self.splitOutline.sizes()
|
||||
|
||||
def setTreeFocus(self):
|
||||
"""Set the focus to the tree widget.
|
||||
"""
|
||||
return self.outlineTree.setFocus()
|
||||
|
||||
def treeHasFocus(self):
|
||||
"""Check if the outline tree has focus.
|
||||
"""
|
||||
return self.outlineTree.hasFocus()
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
@@ -172,7 +208,7 @@ class GuiOutlineToolBar(QToolBar):
|
||||
viewColumnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QTreeWidget.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineToolBar ...")
|
||||
|
||||
@@ -187,7 +223,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.setMovable(False)
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
|
||||
stretch = QWidget(self)
|
||||
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
@@ -202,7 +237,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
|
||||
# Actions
|
||||
self.aRefresh = QAction(self.tr("Refresh"), self)
|
||||
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.aRefresh.triggered.connect(self._refreshRequested)
|
||||
|
||||
# Column Menu
|
||||
@@ -212,7 +246,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
)
|
||||
|
||||
self.tbColumns = QToolButton(self)
|
||||
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
|
||||
self.tbColumns.setMenu(self.mColumns)
|
||||
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -224,6 +257,8 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.addWidget(self.tbColumns)
|
||||
self.addWidget(stretch)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiOutlineToolBar initialisation complete")
|
||||
|
||||
return
|
||||
@@ -232,6 +267,16 @@ class GuiOutlineToolBar(QToolBar):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
|
||||
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
|
||||
|
||||
return
|
||||
|
||||
def populateNovelList(self):
|
||||
"""Fill the novel combo box with a list of all novel folders.
|
||||
"""
|
||||
@@ -243,6 +288,17 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
|
||||
return
|
||||
|
||||
def setCurrentRoot(self, rootHandle):
|
||||
"""Set the current active root handle.
|
||||
"""
|
||||
if rootHandle is None:
|
||||
rootIdx = self.novelValue.count() - 1
|
||||
else:
|
||||
rootIdx = self.novelValue.findData(rootHandle)
|
||||
if rootIdx >= 0:
|
||||
self.novelValue.setCurrentIndex(rootIdx)
|
||||
return
|
||||
|
||||
def setColumnHiddenState(self, hiddenState):
|
||||
"""Forward the change of column hidden states to the menu.
|
||||
"""
|
||||
@@ -313,11 +369,14 @@ class GuiOutlineTree(QTreeWidget):
|
||||
nwOutline.SYNOP: False,
|
||||
}
|
||||
|
||||
D_HANDLE = Qt.UserRole
|
||||
D_TITLE = Qt.UserRole + 1
|
||||
|
||||
hiddenStateChanged = pyqtSignal()
|
||||
activeItemChanged = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QTreeWidget.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineTree ...")
|
||||
|
||||
@@ -326,6 +385,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self.theProject = theOutline.mainGui.theProject
|
||||
self.mainTheme = theOutline.mainGui.mainTheme
|
||||
|
||||
self.setUniformRowHeights(True)
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
@@ -336,11 +396,28 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setIndentation(iPx)
|
||||
self.setIndentation(0)
|
||||
|
||||
self.treeHead = self.header()
|
||||
self.treeHead.sectionMoved.connect(self._columnMoved)
|
||||
|
||||
# Pre-Generate Tree Formatting
|
||||
fH1 = self.font()
|
||||
fH1.setBold(True)
|
||||
fH1.setUnderline(True)
|
||||
|
||||
fH2 = self.font()
|
||||
fH2.setBold(True)
|
||||
|
||||
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||||
self._dIcon = {
|
||||
"H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
|
||||
"H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
|
||||
"H2": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
|
||||
"H3": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
|
||||
"H4": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
|
||||
}
|
||||
|
||||
# Internals
|
||||
self._treeOrder = []
|
||||
self._colWidth = {}
|
||||
@@ -350,8 +427,8 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self._firstView = True
|
||||
self._lastBuild = 0
|
||||
|
||||
self.initOutline()
|
||||
self.clearOutline()
|
||||
self.initSettings()
|
||||
self.clearContent()
|
||||
|
||||
self.hiddenStateChanged.emit()
|
||||
|
||||
@@ -371,7 +448,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def initOutline(self):
|
||||
def initSettings(self):
|
||||
"""Set or update outline settings.
|
||||
"""
|
||||
# Scroll bars
|
||||
@@ -387,7 +464,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def clearOutline(self):
|
||||
def clearContent(self):
|
||||
"""Clear the tree and header and set the default values for the
|
||||
columns arrays.
|
||||
"""
|
||||
@@ -425,18 +502,20 @@ class GuiOutlineTree(QTreeWidget):
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# was last built, we rebuild the tree from the updated index.
|
||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
self._populateTree(rootHandle)
|
||||
if not (novelChanged or indexChanged or overRide):
|
||||
logger.debug("No changes have been made to the novel index")
|
||||
return
|
||||
|
||||
self._populateTree(rootHandle)
|
||||
self.theProject.data.setLastHandle(rootHandle or None, "outline")
|
||||
|
||||
return
|
||||
|
||||
def closeOutline(self):
|
||||
def closeProjectTasks(self):
|
||||
"""Called before a project is closed.
|
||||
"""
|
||||
self._saveHeaderState()
|
||||
self.clearOutline()
|
||||
self.clearContent()
|
||||
self._firstView = True
|
||||
return
|
||||
|
||||
@@ -448,7 +527,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
if selItem:
|
||||
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
|
||||
|
||||
return tHandle, tLine
|
||||
@@ -474,8 +553,8 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"""
|
||||
selItems = self.selectedItems()
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
self.activeItemChanged.emit(tHandle, sTitle)
|
||||
|
||||
return
|
||||
@@ -494,11 +573,9 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"""Receive the changes to column visibility forwarded by the
|
||||
column selection menu.
|
||||
"""
|
||||
logger.verbose("User toggled Outline column '%s'", theItem.name)
|
||||
if theItem in self._colIdx:
|
||||
self.setColumnHidden(self._colIdx[theItem], not isChecked)
|
||||
self._saveHeaderState()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -613,110 +690,60 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
||||
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
||||
|
||||
# Make sure title column is always visible,
|
||||
# and handle column always hidden
|
||||
# Make sure title column is always visible
|
||||
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
||||
|
||||
headItem = self.headerItem()
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
currTitle = None
|
||||
currChapter = None
|
||||
currScene = None
|
||||
if headItem is not None:
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||
for _, tHandle, sTitle, novIdx in novStruct:
|
||||
|
||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
||||
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
|
||||
if iLevel == 0:
|
||||
continue
|
||||
|
||||
tLevel = novIdx.level
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
currChapter = None
|
||||
currScene = None
|
||||
trItem = QTreeWidgetItem()
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
elif tLevel == "H2":
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
currChapter = tItem
|
||||
currScene = None
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
|
||||
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
|
||||
trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
|
||||
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||||
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
|
||||
trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||||
trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
||||
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
|
||||
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
|
||||
trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
|
||||
trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
elif tLevel == "H3":
|
||||
if currChapter is None:
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
else:
|
||||
currChapter.addChild(tItem)
|
||||
currScene = tItem
|
||||
refs = self.theProject.index.getReferences(tHandle, sTitle)
|
||||
trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY]))
|
||||
|
||||
elif tLevel == "H4":
|
||||
if currScene is None:
|
||||
if currChapter is None:
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
else:
|
||||
currChapter.addChild(tItem)
|
||||
else:
|
||||
currScene.addChild(tItem)
|
||||
|
||||
tItem.setExpanded(True)
|
||||
self.addTopLevelItem(trItem)
|
||||
|
||||
self._lastBuild = time()
|
||||
|
||||
return
|
||||
|
||||
def _createTreeItem(self, tHandle, sTitle, novIdx):
|
||||
"""Populate a tree item with all the column values.
|
||||
"""
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % novIdx.level.lower()
|
||||
|
||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
||||
dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel)
|
||||
|
||||
cC = int(novIdx.charCount)
|
||||
wC = int(novIdx.wordCount)
|
||||
pC = int(novIdx.paraCount)
|
||||
|
||||
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
||||
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
|
||||
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon))
|
||||
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||||
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||||
newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
|
||||
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
|
||||
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
||||
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
|
||||
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
|
||||
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
|
||||
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
||||
|
||||
return newItem
|
||||
|
||||
# END Class GuiOutlineTree
|
||||
|
||||
|
||||
@@ -725,7 +752,7 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
columnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QMenu.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
self.acceptToggle = True
|
||||
|
||||
@@ -776,7 +803,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
itemTagClicked = pyqtSignal(str)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QScrollArea.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineDetails ...")
|
||||
|
||||
@@ -963,13 +990,13 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.setWidgetResizable(True)
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
|
||||
self.initDetails()
|
||||
self.initSettings()
|
||||
|
||||
logger.debug("GuiOutlineDetails initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def initDetails(self):
|
||||
def initSettings(self):
|
||||
"""Set or update outline settings.
|
||||
"""
|
||||
# Scroll bars
|
||||
@@ -1032,7 +1059,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||
self.titleValue.setText(novIdx.title)
|
||||
|
||||
itemStatus, _ = nwItem.getImportStatus()
|
||||
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
||||
|
||||
self.fileValue.setText(nwItem.itemName)
|
||||
self.itemValue.setText(itemStatus)
|
||||
|
||||
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiMainStatus(QStatusBar):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QStatusBar.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiMainStatus ...")
|
||||
|
||||
@@ -66,7 +66,6 @@ class GuiMainStatus(QStatusBar):
|
||||
# The Spell Checker Language
|
||||
self.langIcon = QLabel("")
|
||||
self.langText = QLabel(self.tr("None"))
|
||||
self.langIcon.setPixmap(self.mainTheme.getPixmap("status_lang", (iPx, iPx)))
|
||||
self.langIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.langText.setContentsMargins(0, 0, xM, 0)
|
||||
self.addPermanentWidget(self.langIcon)
|
||||
@@ -91,7 +90,6 @@ class GuiMainStatus(QStatusBar):
|
||||
# The Project and Session Stats
|
||||
self.statsIcon = QLabel()
|
||||
self.statsText = QLabel("")
|
||||
self.statsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (iPx, iPx)))
|
||||
self.statsIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.statsText.setContentsMargins(0, 0, xM, 0)
|
||||
self.addPermanentWidget(self.statsIcon)
|
||||
@@ -99,12 +97,8 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
# The Session Clock
|
||||
# Set the mimimum width so the label doesn't rescale every second
|
||||
self.timePixmap = self.mainTheme.getPixmap("status_time", (iPx, iPx))
|
||||
self.idlePixmap = self.mainTheme.getPixmap("status_idle", (iPx, iPx))
|
||||
|
||||
self.timeIcon = QLabel()
|
||||
self.timeText = QLabel("")
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
self.timeText.setToolTip(self.tr("Session Time"))
|
||||
self.timeText.setMinimumWidth(self.mainTheme.getTextWidth("00:00:00:"))
|
||||
self.timeIcon.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -117,6 +111,7 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
logger.debug("GuiMainStatus initialisation complete")
|
||||
|
||||
self.updateTheme()
|
||||
self.clearStatus()
|
||||
|
||||
return
|
||||
@@ -132,6 +127,21 @@ class GuiMainStatus(QStatusBar):
|
||||
self.updateTime()
|
||||
return True
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
|
||||
self.langIcon.setPixmap(self.mainTheme.getPixmap("status_lang", (iPx, iPx)))
|
||||
self.statsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (iPx, iPx)))
|
||||
|
||||
self.timePixmap = self.mainTheme.getPixmap("status_time", (iPx, iPx))
|
||||
self.idlePixmap = self.mainTheme.getPixmap("status_idle", (iPx, iPx))
|
||||
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
@@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
@@ -38,7 +37,7 @@ from PyQt5.QtGui import (
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import NWConfigParser, readTextFile
|
||||
from novelwriter.common import NWConfigParser, minmax
|
||||
from novelwriter.constants import nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -67,6 +66,7 @@ class GuiTheme:
|
||||
self.themeUrl = ""
|
||||
self.themeLicense = ""
|
||||
self.themeLicenseUrl = ""
|
||||
self.themeIcons = ""
|
||||
|
||||
# GUI
|
||||
self.statNone = [120, 120, 120]
|
||||
@@ -104,43 +104,41 @@ class GuiTheme:
|
||||
self.colRepTag = [0, 0, 0]
|
||||
self.colMod = [0, 0, 0]
|
||||
|
||||
# Changeable Settings
|
||||
self.guiTheme = None
|
||||
self.guiSyntax = None
|
||||
self.syntaxFile = None
|
||||
self.cssFile = None
|
||||
self.guiFontDB = QFontDatabase()
|
||||
|
||||
# Class Setup
|
||||
# ===========
|
||||
|
||||
# Init GUI Font
|
||||
self.guiFontDB = QFontDatabase()
|
||||
self._setGuiFont()
|
||||
|
||||
# Load Themes
|
||||
self._guiPalette = QPalette()
|
||||
self._themeList = []
|
||||
self._syntaxList = []
|
||||
self._availThemes = {}
|
||||
self._availSyntax = {}
|
||||
|
||||
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
|
||||
self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax"))
|
||||
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
|
||||
self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes"))
|
||||
self._listConf(self._availSyntax, self.mainConf.assetPath("syntax"))
|
||||
self._listConf(self._availThemes, self.mainConf.assetPath("themes"))
|
||||
self._listConf(self._availSyntax, self.mainConf.dataPath("syntax"))
|
||||
self._listConf(self._availThemes, self.mainConf.dataPath("themes"))
|
||||
|
||||
self.updateFont()
|
||||
self.updateTheme()
|
||||
self.iconCache.updateTheme()
|
||||
self.loadTheme()
|
||||
self.loadSyntax()
|
||||
|
||||
# Icon Functions
|
||||
self.getIcon = self.iconCache.getIcon
|
||||
self.getPixmap = self.iconCache.getPixmap
|
||||
self.getItemIcon = self.iconCache.getItemIcon
|
||||
self.loadDecoration = self.iconCache.loadDecoration
|
||||
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
||||
|
||||
# Extract Other Info
|
||||
self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
|
||||
self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
|
||||
self.mainConf.guiScale = self.guiScale
|
||||
logger.verbose("GUI DPI: %.1f", self.guiDPI)
|
||||
logger.verbose("GUI Scale: %.2f", self.guiScale)
|
||||
logger.debug("GUI DPI: %.1f", self.guiDPI)
|
||||
logger.debug("GUI Scale: %.2f", self.guiScale)
|
||||
|
||||
# Fonts
|
||||
self.guiFont = qApp.font()
|
||||
@@ -157,12 +155,12 @@ class GuiTheme:
|
||||
self.guiFontFixed.setPointSizeF(0.95*self.fontPointSize)
|
||||
self.guiFontFixed.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
|
||||
|
||||
logger.verbose("GUI Font Family: %s", self.guiFont.family())
|
||||
logger.verbose("GUI Font Point Size: %.2f", self.fontPointSize)
|
||||
logger.verbose("GUI Font Pixel Size: %d", self.fontPixelSize)
|
||||
logger.verbose("GUI Base Icon Size: %d", self.baseIconSize)
|
||||
logger.verbose("Text 'N' Height: %d", self.textNHeight)
|
||||
logger.verbose("Text 'N' Width: %d", self.textNWidth)
|
||||
logger.debug("GUI Font Family: %s", self.guiFont.family())
|
||||
logger.debug("GUI Font Point Size: %.2f", self.fontPointSize)
|
||||
logger.debug("GUI Font Pixel Size: %d", self.fontPixelSize)
|
||||
logger.debug("GUI Base Icon Size: %d", self.baseIconSize)
|
||||
logger.debug("Text 'N' Height: %d", self.textNHeight)
|
||||
logger.debug("Text 'N' Width: %d", self.textNWidth)
|
||||
|
||||
return
|
||||
|
||||
@@ -180,10 +178,192 @@ class GuiTheme:
|
||||
return int(ceil(qMetrics.boundingRect(theText).width()))
|
||||
|
||||
##
|
||||
# Actions
|
||||
# Theme Methods
|
||||
##
|
||||
|
||||
def updateFont(self):
|
||||
def loadTheme(self):
|
||||
"""Load the currently specified GUI theme.
|
||||
"""
|
||||
guiTheme = self.mainConf.guiTheme
|
||||
if guiTheme not in self._availThemes:
|
||||
logger.error("Could not find GUI theme '%s'", guiTheme)
|
||||
guiTheme = "default"
|
||||
self.mainConf.guiTheme = guiTheme
|
||||
|
||||
themeFile = self._availThemes.get(guiTheme, None)
|
||||
if themeFile is None:
|
||||
logger.error("Could not load GUI theme")
|
||||
return False
|
||||
|
||||
# Config File
|
||||
logger.info("Loading GUI theme '%s'", guiTheme)
|
||||
confParser = NWConfigParser()
|
||||
try:
|
||||
with open(themeFile, mode="r", encoding="utf-8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception:
|
||||
logger.error("Could not load theme settings from: %s", themeFile)
|
||||
logException()
|
||||
return False
|
||||
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.themeName = confParser.rdStr(cnfSec, "name", "")
|
||||
self.themeDescription = confParser.rdStr(cnfSec, "description", "N/A")
|
||||
self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A")
|
||||
self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A")
|
||||
self.themeUrl = confParser.rdStr(cnfSec, "url", "")
|
||||
self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A")
|
||||
self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
|
||||
self.themeIcons = confParser.rdStr(cnfSec, "icontheme", "")
|
||||
|
||||
# Palette
|
||||
cnfSec = "Palette"
|
||||
if confParser.has_section(cnfSec):
|
||||
self._setPalette(confParser, cnfSec, "window", QPalette.Window)
|
||||
self._setPalette(confParser, cnfSec, "windowtext", QPalette.WindowText)
|
||||
self._setPalette(confParser, cnfSec, "base", QPalette.Base)
|
||||
self._setPalette(confParser, cnfSec, "alternatebase", QPalette.AlternateBase)
|
||||
self._setPalette(confParser, cnfSec, "text", QPalette.Text)
|
||||
self._setPalette(confParser, cnfSec, "tooltipbase", QPalette.ToolTipBase)
|
||||
self._setPalette(confParser, cnfSec, "tooltiptext", QPalette.ToolTipText)
|
||||
self._setPalette(confParser, cnfSec, "button", QPalette.Button)
|
||||
self._setPalette(confParser, cnfSec, "buttontext", QPalette.ButtonText)
|
||||
self._setPalette(confParser, cnfSec, "brighttext", QPalette.BrightText)
|
||||
self._setPalette(confParser, cnfSec, "highlight", QPalette.Highlight)
|
||||
self._setPalette(confParser, cnfSec, "highlightedtext", QPalette.HighlightedText)
|
||||
self._setPalette(confParser, cnfSec, "link", QPalette.Link)
|
||||
self._setPalette(confParser, cnfSec, "linkvisited", QPalette.LinkVisited)
|
||||
else:
|
||||
self._guiPalette = qApp.style().standardPalette()
|
||||
|
||||
# GUI
|
||||
cnfSec = "GUI"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.statNone = self._parseColour(confParser, cnfSec, "statusnone")
|
||||
self.statUnsaved = self._parseColour(confParser, cnfSec, "statusunsaved")
|
||||
self.statSaved = self._parseColour(confParser, cnfSec, "statussaved")
|
||||
|
||||
# Icons
|
||||
self.iconCache.loadTheme(self.themeIcons)
|
||||
|
||||
# Update Dependant Colours
|
||||
backCol = self._guiPalette.window().color()
|
||||
textCol = self._guiPalette.windowText().color()
|
||||
|
||||
backLCol = backCol.lightnessF()
|
||||
textLCol = textCol.lightnessF()
|
||||
|
||||
if backLCol > textLCol:
|
||||
helpLCol = textLCol + 0.65*(backLCol - textLCol)
|
||||
else:
|
||||
helpLCol = backLCol + 0.65*(textLCol - backLCol)
|
||||
|
||||
self.helpText = [int(255*helpLCol)]*3
|
||||
|
||||
# Apply Styles
|
||||
qApp.setPalette(self._guiPalette)
|
||||
|
||||
return True
|
||||
|
||||
def loadSyntax(self):
|
||||
"""Load the currently specified syntax highlighter theme.
|
||||
"""
|
||||
guiSyntax = self.mainConf.guiSyntax
|
||||
if guiSyntax not in self._availSyntax:
|
||||
logger.error("Could not find syntax theme '%s'", guiSyntax)
|
||||
guiSyntax = "default_light"
|
||||
self.mainConf.guiSyntax = guiSyntax
|
||||
|
||||
syntaxFile = self._availSyntax.get(guiSyntax, None)
|
||||
if syntaxFile is None:
|
||||
logger.error("Could not load syntax theme")
|
||||
return False
|
||||
|
||||
logger.info("Loading syntax theme '%s'", guiSyntax)
|
||||
|
||||
confParser = NWConfigParser()
|
||||
try:
|
||||
with open(syntaxFile, mode="r", encoding="utf-8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception:
|
||||
logger.error("Could not load syntax colours from: %s", syntaxFile)
|
||||
logException()
|
||||
return False
|
||||
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.syntaxName = confParser.rdStr(cnfSec, "name", "")
|
||||
self.syntaxDescription = confParser.rdStr(cnfSec, "description", "N/A")
|
||||
self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "N/A")
|
||||
self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "N/A")
|
||||
self.syntaxUrl = confParser.rdStr(cnfSec, "url", "")
|
||||
self.syntaxLicense = confParser.rdStr(cnfSec, "license", "N/A")
|
||||
self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
|
||||
|
||||
# Syntax
|
||||
cnfSec = "Syntax"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.colBack = self._parseColour(confParser, cnfSec, "background")
|
||||
self.colText = self._parseColour(confParser, cnfSec, "text")
|
||||
self.colLink = self._parseColour(confParser, cnfSec, "link")
|
||||
self.colHead = self._parseColour(confParser, cnfSec, "headertext")
|
||||
self.colHeadH = self._parseColour(confParser, cnfSec, "headertag")
|
||||
self.colEmph = self._parseColour(confParser, cnfSec, "emphasis")
|
||||
self.colDialN = self._parseColour(confParser, cnfSec, "straightquotes")
|
||||
self.colDialD = self._parseColour(confParser, cnfSec, "doublequotes")
|
||||
self.colDialS = self._parseColour(confParser, cnfSec, "singlequotes")
|
||||
self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
|
||||
self.colKey = self._parseColour(confParser, cnfSec, "keyword")
|
||||
self.colVal = self._parseColour(confParser, cnfSec, "value")
|
||||
self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
|
||||
self.colError = self._parseColour(confParser, cnfSec, "errorline")
|
||||
self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag")
|
||||
self.colMod = self._parseColour(confParser, cnfSec, "modifier")
|
||||
|
||||
return True
|
||||
|
||||
def listThemes(self):
|
||||
"""Scan the GUI themes folder and list all themes.
|
||||
"""
|
||||
if self._themeList:
|
||||
return self._themeList
|
||||
|
||||
confParser = NWConfigParser()
|
||||
for themeKey, themePath in self._availThemes.items():
|
||||
logger.debug("Checking theme config for '%s'", themeKey)
|
||||
themeName = _loadInternalName(confParser, themePath)
|
||||
if themeName:
|
||||
self._themeList.append((themeKey, themeName))
|
||||
|
||||
self._themeList = sorted(self._themeList, key=lambda x: x[1])
|
||||
|
||||
return self._themeList
|
||||
|
||||
def listSyntax(self):
|
||||
"""Scan the syntax themes folder and list all themes.
|
||||
"""
|
||||
if self._syntaxList:
|
||||
return self._syntaxList
|
||||
|
||||
confParser = NWConfigParser()
|
||||
for syntaxKey, syntaxPath in self._availSyntax.items():
|
||||
logger.debug("Checking theme syntax for '%s'", syntaxKey)
|
||||
syntaxName = _loadInternalName(confParser, syntaxPath)
|
||||
if syntaxName:
|
||||
self._syntaxList.append((syntaxKey, syntaxName))
|
||||
|
||||
self._syntaxList = sorted(self._syntaxList, key=lambda x: x[1])
|
||||
|
||||
return self._syntaxList
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setGuiFont(self):
|
||||
"""Update the GUI's font style from settings.
|
||||
"""
|
||||
theFont = QFont()
|
||||
@@ -204,233 +384,42 @@ class GuiTheme:
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update the GUI theme from theme files.
|
||||
"""
|
||||
self.guiTheme = self.mainConf.guiTheme
|
||||
self.guiSyntax = self.mainConf.guiSyntax
|
||||
|
||||
self.themeFile = self._availThemes.get(self.guiTheme, None)
|
||||
if self.themeFile is None:
|
||||
logger.error("Could not find GUI theme '%s'", self.guiTheme)
|
||||
else:
|
||||
self.cssFile = self.themeFile[:-5]+".css"
|
||||
self.loadTheme()
|
||||
|
||||
self.syntaxFile = self._availSyntax.get(self.guiSyntax, None)
|
||||
if self.syntaxFile is None:
|
||||
logger.error("Could not find syntax theme '%s'", self.guiSyntax)
|
||||
else:
|
||||
self.loadSyntax()
|
||||
|
||||
# Update dependant colours
|
||||
backCol = qApp.palette().window().color()
|
||||
textCol = qApp.palette().windowText().color()
|
||||
|
||||
backLCol = backCol.lightnessF()
|
||||
textLCol = textCol.lightnessF()
|
||||
|
||||
if backLCol > textLCol:
|
||||
helpLCol = textLCol + 0.65*(backLCol - textLCol)
|
||||
else:
|
||||
helpLCol = backLCol + 0.65*(textLCol - backLCol)
|
||||
|
||||
self.helpText = [int(255*helpLCol)]*3
|
||||
|
||||
return True
|
||||
|
||||
def loadTheme(self):
|
||||
"""Load the currently specified GUI theme.
|
||||
"""
|
||||
logger.info("Loading GUI theme '%s'", self.guiTheme)
|
||||
|
||||
# Config File
|
||||
confParser = NWConfigParser()
|
||||
try:
|
||||
with open(self.themeFile, mode="r", encoding="utf-8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception:
|
||||
logger.error("Could not load theme settings from: %s", self.themeFile)
|
||||
logException()
|
||||
return False
|
||||
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.themeName = confParser.rdStr(cnfSec, "name", "")
|
||||
self.themeDescription = confParser.rdStr(cnfSec, "description", "N/A")
|
||||
self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A")
|
||||
self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A")
|
||||
self.themeUrl = confParser.rdStr(cnfSec, "url", "")
|
||||
self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A")
|
||||
self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
|
||||
|
||||
# Palette
|
||||
cnfSec = "Palette"
|
||||
if confParser.has_section(cnfSec):
|
||||
self._setPalette(confParser, cnfSec, "window", QPalette.Window)
|
||||
self._setPalette(confParser, cnfSec, "windowtext", QPalette.WindowText)
|
||||
self._setPalette(confParser, cnfSec, "base", QPalette.Base)
|
||||
self._setPalette(confParser, cnfSec, "alternatebase", QPalette.AlternateBase)
|
||||
self._setPalette(confParser, cnfSec, "text", QPalette.Text)
|
||||
self._setPalette(confParser, cnfSec, "tooltipbase", QPalette.ToolTipBase)
|
||||
self._setPalette(confParser, cnfSec, "tooltiptext", QPalette.ToolTipText)
|
||||
self._setPalette(confParser, cnfSec, "button", QPalette.Button)
|
||||
self._setPalette(confParser, cnfSec, "buttontext", QPalette.ButtonText)
|
||||
self._setPalette(confParser, cnfSec, "brighttext", QPalette.BrightText)
|
||||
self._setPalette(confParser, cnfSec, "highlight", QPalette.Highlight)
|
||||
self._setPalette(confParser, cnfSec, "highlightedtext", QPalette.HighlightedText)
|
||||
self._setPalette(confParser, cnfSec, "link", QPalette.Link)
|
||||
self._setPalette(confParser, cnfSec, "linkvisited", QPalette.LinkVisited)
|
||||
|
||||
# GUI
|
||||
cnfSec = "GUI"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.statNone = self._loadColour(confParser, cnfSec, "statusnone")
|
||||
self.statUnsaved = self._loadColour(confParser, cnfSec, "statusunsaved")
|
||||
self.statSaved = self._loadColour(confParser, cnfSec, "statussaved")
|
||||
|
||||
# CSS File
|
||||
cssData = readTextFile(self.cssFile)
|
||||
if cssData:
|
||||
qApp.setStyleSheet(cssData)
|
||||
|
||||
# Apply Styles
|
||||
qApp.setPalette(self._guiPalette)
|
||||
|
||||
return True
|
||||
|
||||
def loadSyntax(self):
|
||||
"""Load the currently specified syntax highlighter theme.
|
||||
"""
|
||||
logger.info("Loading syntax theme '%s'", self.guiSyntax)
|
||||
|
||||
confParser = NWConfigParser()
|
||||
try:
|
||||
with open(self.syntaxFile, mode="r", encoding="utf-8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception:
|
||||
logger.error("Could not load syntax colours from: %s", self.syntaxFile)
|
||||
logException()
|
||||
return False
|
||||
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.syntaxName = confParser.rdStr(cnfSec, "name", "")
|
||||
self.syntaxDescription = confParser.rdStr(cnfSec, "description", "")
|
||||
self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "")
|
||||
self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "")
|
||||
self.syntaxUrl = confParser.rdStr(cnfSec, "url", "")
|
||||
self.syntaxLicense = confParser.rdStr(cnfSec, "license", "")
|
||||
self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
|
||||
|
||||
# Syntax
|
||||
cnfSec = "Syntax"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.colBack = self._loadColour(confParser, cnfSec, "background")
|
||||
self.colText = self._loadColour(confParser, cnfSec, "text")
|
||||
self.colLink = self._loadColour(confParser, cnfSec, "link")
|
||||
self.colHead = self._loadColour(confParser, cnfSec, "headertext")
|
||||
self.colHeadH = self._loadColour(confParser, cnfSec, "headertag")
|
||||
self.colEmph = self._loadColour(confParser, cnfSec, "emphasis")
|
||||
self.colDialN = self._loadColour(confParser, cnfSec, "straightquotes")
|
||||
self.colDialD = self._loadColour(confParser, cnfSec, "doublequotes")
|
||||
self.colDialS = self._loadColour(confParser, cnfSec, "singlequotes")
|
||||
self.colHidden = self._loadColour(confParser, cnfSec, "hidden")
|
||||
self.colKey = self._loadColour(confParser, cnfSec, "keyword")
|
||||
self.colVal = self._loadColour(confParser, cnfSec, "value")
|
||||
self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline")
|
||||
self.colError = self._loadColour(confParser, cnfSec, "errorline")
|
||||
self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag")
|
||||
self.colMod = self._loadColour(confParser, cnfSec, "modifier")
|
||||
|
||||
return True
|
||||
|
||||
def listThemes(self):
|
||||
"""Scan the GUI themes folder and list all themes.
|
||||
"""
|
||||
if self._themeList:
|
||||
return self._themeList
|
||||
|
||||
confParser = NWConfigParser()
|
||||
for themeKey, themePath in self._availThemes.items():
|
||||
logger.verbose("Checking theme config for '%s'", themeKey)
|
||||
themeName = _loadInternalName(confParser, themePath)
|
||||
if themeName:
|
||||
self._themeList.append((themeKey, themeName))
|
||||
|
||||
self._themeList = sorted(self._themeList, key=lambda x: x[1])
|
||||
|
||||
return self._themeList
|
||||
|
||||
def listSyntax(self):
|
||||
"""Scan the syntax themes folder and list all themes.
|
||||
"""
|
||||
if self._syntaxList:
|
||||
return self._syntaxList
|
||||
|
||||
confParser = NWConfigParser()
|
||||
for syntaxKey, syntaxPath in self._availSyntax.items():
|
||||
logger.verbose("Checking theme syntax for '%s'", syntaxKey)
|
||||
syntaxName = _loadInternalName(confParser, syntaxPath)
|
||||
if syntaxName:
|
||||
self._syntaxList.append((syntaxKey, syntaxName))
|
||||
|
||||
self._syntaxList = sorted(self._syntaxList, key=lambda x: x[1])
|
||||
|
||||
return self._syntaxList
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _listConf(self, targetDict, checkDir):
|
||||
"""Scan for syntax and gui themes and populate the dictionary.
|
||||
"""Scan for theme config files and populate the dictionary.
|
||||
"""
|
||||
if not os.path.isdir(checkDir):
|
||||
return
|
||||
if not checkDir.is_dir():
|
||||
return False
|
||||
|
||||
for checkFile in os.listdir(checkDir):
|
||||
confPath = os.path.join(checkDir, checkFile)
|
||||
if os.path.isfile(confPath) and confPath.endswith(".conf"):
|
||||
targetDict[checkFile[:-5]] = confPath
|
||||
for checkFile in checkDir.iterdir():
|
||||
if checkFile.is_file() and checkFile.name.endswith(".conf"):
|
||||
targetDict[checkFile.name[:-5]] = checkFile
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _loadColour(self, confParser, cnfSec, cnfName):
|
||||
"""Load a colour value from a config string.
|
||||
def _parseColour(self, confParser, cnfSec, cnfName):
|
||||
"""Parse a colour value from a config string.
|
||||
"""
|
||||
if confParser.has_option(cnfSec, cnfName):
|
||||
inData = confParser.get(cnfSec, cnfName).split(",")
|
||||
outData = []
|
||||
values = confParser.get(cnfSec, cnfName).split(",")
|
||||
result = []
|
||||
try:
|
||||
outData.append(int(inData[0]))
|
||||
outData.append(int(inData[1]))
|
||||
outData.append(int(inData[2]))
|
||||
result.append(minmax(int(values[0]), 0, 255))
|
||||
result.append(minmax(int(values[1]), 0, 255))
|
||||
result.append(minmax(int(values[2]), 0, 255))
|
||||
except Exception:
|
||||
logger.error("Could not load theme colours for '%s' from config file", cnfName)
|
||||
outData = [0, 0, 0]
|
||||
result = [0, 0, 0]
|
||||
else:
|
||||
logger.warning("Could not find theme colours for '%s' in config file", cnfName)
|
||||
outData = [0, 0, 0]
|
||||
return outData
|
||||
result = [0, 0, 0]
|
||||
return result
|
||||
|
||||
def _setPalette(self, confParser, cnfSec, cnfName, paletteVal):
|
||||
"""Set a palette colour value from a config string.
|
||||
"""
|
||||
readCol = []
|
||||
if confParser.has_option(cnfSec, cnfName):
|
||||
inData = confParser.get(cnfSec, cnfName).split(",")
|
||||
try:
|
||||
readCol.append(int(inData[0]))
|
||||
readCol.append(int(inData[1]))
|
||||
readCol.append(int(inData[2]))
|
||||
except Exception:
|
||||
logger.error("Could not load theme colours for '%s' from config file", cnfName)
|
||||
return
|
||||
if len(readCol) == 3:
|
||||
self._guiPalette.setColor(paletteVal, QColor(*readCol))
|
||||
self._guiPalette.setColor(
|
||||
paletteVal, QColor(*self._parseColour(confParser, cnfSec, cnfName))
|
||||
)
|
||||
return
|
||||
|
||||
# End Class GuiTheme
|
||||
@@ -457,24 +446,24 @@ class GuiIcons:
|
||||
ICON_KEYS = {
|
||||
# Project and GUI icons
|
||||
"novelwriter", "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none",
|
||||
"cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", "doc_h0",
|
||||
"doc_h1", "doc_h2", "doc_h3", "doc_h4", "proj_chapter", "proj_details", "proj_document",
|
||||
"proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title",
|
||||
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
|
||||
"search_regex", "search_word", "status_idle", "status_lang", "status_lines",
|
||||
"status_stats", "status_time", "view_build", "view_editor", "view_novel", "view_outline",
|
||||
"cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
|
||||
"proj_chapter", "proj_details", "proj_document", "proj_folder", "proj_note", "proj_nwx",
|
||||
"proj_section", "proj_scene", "proj_stats", "proj_title", "search_cancel", "search_case",
|
||||
"search_loop", "search_preserve", "search_project", "search_regex", "search_word",
|
||||
"status_idle", "status_lang", "status_lines", "status_stats", "status_time", "view_build",
|
||||
"view_editor", "view_novel", "view_outline",
|
||||
|
||||
# General Button Icons
|
||||
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
|
||||
"forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
|
||||
"save", "search_replace", "search", "settings", "up",
|
||||
"add", "backward", "bookmark", "checked", "close", "cross", "down", "edit", "forward",
|
||||
"maximise", "menu", "minimise", "noncheckable", "reference", "refresh", "remove",
|
||||
"search_replace", "search", "settings", "unchecked", "up",
|
||||
|
||||
# Switches
|
||||
"sticky-on", "sticky-off",
|
||||
"bullet-on", "bullet-off",
|
||||
|
||||
# Decorations
|
||||
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4",
|
||||
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
|
||||
}
|
||||
|
||||
IMAGE_MAP = {
|
||||
@@ -489,12 +478,11 @@ class GuiIcons:
|
||||
# Storage
|
||||
self._qIcons = {}
|
||||
self._themeMap = {}
|
||||
self._themeList = []
|
||||
self._headerDec = []
|
||||
self._confName = "icons.conf"
|
||||
|
||||
# Icon Theme Path
|
||||
self._iconPath = os.path.join(self.mainConf.assetPath, "icons")
|
||||
self._themePath = os.path.join(self._iconPath, "system")
|
||||
self._iconPath = self.mainConf.assetPath("icons")
|
||||
|
||||
# Icon Theme Meta
|
||||
self.themeName = ""
|
||||
@@ -511,20 +499,19 @@ class GuiIcons:
|
||||
# Actions
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
def loadTheme(self, iconTheme):
|
||||
"""Update the theme map. This is more of an init, since many of
|
||||
the GUI icons cannot really be replaced without writing specific
|
||||
update functions for the classes where they're used.
|
||||
"""
|
||||
self._themeMap = {}
|
||||
themePath = self._getThemePath()
|
||||
if themePath is None:
|
||||
logger.warning("No icons loaded")
|
||||
themePath = self._iconPath / iconTheme
|
||||
if not themePath.is_dir():
|
||||
logger.warning("No icons loaded for '%s'", iconTheme)
|
||||
return False
|
||||
|
||||
self._themePath = themePath
|
||||
themeConf = os.path.join(themePath, self._confName)
|
||||
logger.info("Loading icon theme '%s'", self.mainConf.guiIcons)
|
||||
themeConf = themePath / self._confName
|
||||
logger.info("Loading icon theme '%s'", iconTheme)
|
||||
|
||||
# Config File
|
||||
confParser = NWConfigParser()
|
||||
@@ -554,10 +541,10 @@ class GuiIcons:
|
||||
if iconName not in self.ICON_KEYS:
|
||||
logger.error("Unknown icon name '%s' in config file", iconName)
|
||||
else:
|
||||
iconPath = os.path.join(self._themePath, iconFile)
|
||||
if os.path.isfile(iconPath):
|
||||
iconPath = themePath / iconFile
|
||||
if iconPath.is_file():
|
||||
self._themeMap[iconName] = iconPath
|
||||
logger.verbose("Icon slot '%s' using file '%s'", iconName, iconFile)
|
||||
logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile)
|
||||
else:
|
||||
logger.error("Icon file '%s' not in theme folder", iconFile)
|
||||
|
||||
@@ -570,6 +557,14 @@ class GuiIcons:
|
||||
if iconKey not in self._themeMap:
|
||||
logger.error("No icon file specified for '%s'", iconKey)
|
||||
|
||||
# Refresh icons
|
||||
for iconKey in self._qIcons:
|
||||
logger.debug("Reloading icon: '%s'", iconKey)
|
||||
qIcon = self._loadIcon(iconKey)
|
||||
self._qIcons[iconKey] = qIcon
|
||||
|
||||
self._headerDec = []
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
@@ -583,18 +578,16 @@ class GuiIcons:
|
||||
if decoKey in self._themeMap:
|
||||
imgPath = self._themeMap[decoKey]
|
||||
elif decoKey in self.IMAGE_MAP:
|
||||
imgPath = os.path.join(
|
||||
self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey]
|
||||
)
|
||||
imgPath = self.mainConf.assetPath("images") / self.IMAGE_MAP[decoKey]
|
||||
else:
|
||||
logger.error("Decoration with name '%s' does not exist", decoKey)
|
||||
return QPixmap()
|
||||
|
||||
if not os.path.isfile(imgPath):
|
||||
logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey])
|
||||
if not imgPath.is_file():
|
||||
logger.error("Asset not found: %s", imgPath)
|
||||
return QPixmap()
|
||||
|
||||
theDeco = QPixmap(imgPath)
|
||||
theDeco = QPixmap(str(imgPath))
|
||||
if pxW is not None and pxH is not None:
|
||||
return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||
elif pxW is None and pxH is not None:
|
||||
@@ -604,7 +597,7 @@ class GuiIcons:
|
||||
|
||||
return theDeco
|
||||
|
||||
def getIcon(self, iconKey, iconSize=None):
|
||||
def getIcon(self, iconKey):
|
||||
"""Return an icon from the icon buffer. If it doesn't exist,
|
||||
return, load it, and if it still doesn't exist, return an empty
|
||||
icon.
|
||||
@@ -641,6 +634,8 @@ class GuiIcons:
|
||||
iconName = "proj_chapter"
|
||||
elif hLevel == "H3":
|
||||
iconName = "proj_scene"
|
||||
elif hLevel == "H4":
|
||||
iconName = "proj_section"
|
||||
elif tLayout == nwItemLayout.NOTE:
|
||||
iconName = "proj_note"
|
||||
if iconName is None:
|
||||
@@ -648,49 +643,24 @@ class GuiIcons:
|
||||
|
||||
return self.getIcon(iconName)
|
||||
|
||||
def listThemes(self):
|
||||
"""Scan the icons themes folder and list all themes.
|
||||
def getHeaderDecoration(self, hLevel):
|
||||
"""Get the decoration for a specific header level.
|
||||
"""
|
||||
if self._themeList:
|
||||
return self._themeList
|
||||
|
||||
confParser = NWConfigParser()
|
||||
for themeDir in os.listdir(self._iconPath):
|
||||
themePath = os.path.join(self._iconPath, themeDir)
|
||||
if not os.path.isdir(themePath):
|
||||
continue
|
||||
|
||||
logger.verbose("Checking icon theme config for '%s'", themeDir)
|
||||
themeConf = os.path.join(themePath, self._confName)
|
||||
themeName = _loadInternalName(confParser, themeConf)
|
||||
if themeName:
|
||||
self._themeList.append((themeDir, themeName))
|
||||
|
||||
self._themeList = sorted(self._themeList, key=lambda x: x[1])
|
||||
|
||||
return self._themeList
|
||||
if not self._headerDec:
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
self._headerDec = [
|
||||
self.loadDecoration("deco_doc_h0", pxH=iPx),
|
||||
self.loadDecoration("deco_doc_h1", pxH=iPx),
|
||||
self.loadDecoration("deco_doc_h2", pxH=iPx),
|
||||
self.loadDecoration("deco_doc_h3", pxH=iPx),
|
||||
self.loadDecoration("deco_doc_h4", pxH=iPx),
|
||||
]
|
||||
return self._headerDec[minmax(hLevel, 0, 4)]
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _getThemePath(self):
|
||||
"""Get a valid theme path. Returns None if it fails.
|
||||
"""
|
||||
themePath = os.path.join(self.mainConf.assetPath, "icons", self.mainConf.guiIcons)
|
||||
if not os.path.isdir(themePath):
|
||||
logger.warning(
|
||||
"Icon theme '%s' not found, resetting to default", self.mainConf.guiIcons
|
||||
)
|
||||
self.mainConf.setDefaultIconTheme()
|
||||
|
||||
themePath = os.path.join(self.mainConf.assetPath, "icons", self.mainConf.guiIcons)
|
||||
if not os.path.isdir(themePath):
|
||||
logger.error("Default icon theme not found")
|
||||
return None
|
||||
|
||||
return themePath
|
||||
|
||||
def _loadIcon(self, iconKey):
|
||||
"""Load an icon from the assets themes folder. Is guaranteed to
|
||||
return a QIcon.
|
||||
@@ -701,15 +671,14 @@ class GuiIcons:
|
||||
|
||||
# If we just want the app icons, return right away
|
||||
if iconKey == "novelwriter":
|
||||
return QIcon(os.path.join(self._iconPath, "novelwriter.svg"))
|
||||
return QIcon(str(self._iconPath / "novelwriter.svg"))
|
||||
elif iconKey == "proj_nwx":
|
||||
return QIcon(os.path.join(self._iconPath, "x-novelwriter-project.svg"))
|
||||
return QIcon(str(self._iconPath / "x-novelwriter-project.svg"))
|
||||
|
||||
# Otherwise, we load from the theme folder
|
||||
if iconKey in self._themeMap:
|
||||
relPath = os.path.relpath(self._themeMap[iconKey], self._iconPath)
|
||||
logger.verbose("Loading: %s", relPath)
|
||||
return QIcon(self._themeMap[iconKey])
|
||||
logger.debug("Loading: %s", self._themeMap[iconKey].name)
|
||||
return QIcon(str(self._themeMap[iconKey]))
|
||||
|
||||
# If we didn't find one, give up and return an empty icon
|
||||
logger.warning("Did not load an icon for '%s'", iconKey)
|
||||
|
||||
@@ -41,7 +41,7 @@ class GuiViewsBar(QToolBar):
|
||||
viewChangeRequested = pyqtSignal(nwView)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QToolBar.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiViewsBar ...")
|
||||
|
||||
@@ -61,63 +61,52 @@ class GuiViewsBar(QToolBar):
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setMaximumWidth(mPx)
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
|
||||
stretch = QWidget(self)
|
||||
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
# Actions
|
||||
self.aProject = QAction(self.tr("Project"))
|
||||
self.aProject = QAction(self.tr("Project"), self)
|
||||
self.aProject.setFont(lblFont)
|
||||
self.aProject.setToolTip(self.tr("Show project tree and editor"))
|
||||
self.aProject.setIcon(self.mainTheme.getIcon("view_editor"))
|
||||
self.aProject.setToolTip(self.tr("Project Tree View"))
|
||||
self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
|
||||
|
||||
self.aNovel = QAction(self.tr("Novel"))
|
||||
self.aNovel = QAction(self.tr("Novel"), self)
|
||||
self.aNovel.setFont(lblFont)
|
||||
self.aNovel.setToolTip(self.tr("Show novel tree and editor"))
|
||||
self.aNovel.setIcon(self.mainTheme.getIcon("view_novel"))
|
||||
self.aNovel.setToolTip(self.tr("Novel Tree View"))
|
||||
self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
|
||||
|
||||
self.aOutline = QAction(self.tr("Outline"))
|
||||
self.aOutline = QAction(self.tr("Outline"), self)
|
||||
self.aOutline.setFont(lblFont)
|
||||
self.aOutline.setToolTip(self.tr("Show novel outline"))
|
||||
self.aOutline.setIcon(self.mainTheme.getIcon("view_outline"))
|
||||
self.aOutline.setToolTip(self.tr("Novel Outline View"))
|
||||
self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
|
||||
|
||||
self.aBuild = QAction(self.tr("Build"))
|
||||
self.aBuild = QAction(self.tr("Build"), self)
|
||||
self.aBuild.setFont(lblFont)
|
||||
self.aBuild.setToolTip(self.tr("Build novel project"))
|
||||
self.aBuild.setIcon(self.mainTheme.getIcon("view_build"))
|
||||
self.aBuild.setToolTip(self.tr("Build Novel Project"))
|
||||
self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog())
|
||||
|
||||
self.aDetails = QAction(self.tr("Details"))
|
||||
self.aDetails = QAction(self.tr("Details"), self)
|
||||
self.aDetails.setFont(lblFont)
|
||||
self.aDetails.setToolTip(self.tr("Show project details"))
|
||||
self.aDetails.setIcon(self.mainTheme.getIcon("proj_details"))
|
||||
self.aDetails.setToolTip(self.tr("Project Details"))
|
||||
self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog())
|
||||
|
||||
self.aStats = QAction(self.tr("Stats"))
|
||||
self.aStats = QAction(self.tr("Stats"), self)
|
||||
self.aStats.setFont(lblFont)
|
||||
self.aStats.setToolTip(self.tr("Show project statistics"))
|
||||
self.aStats.setIcon(self.mainTheme.getIcon("proj_stats"))
|
||||
self.aStats.setToolTip(self.tr("Writing Statistics"))
|
||||
self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog())
|
||||
|
||||
# Settings Menu
|
||||
self.mSettings = QMenu()
|
||||
|
||||
self.aPrjSettings = QAction(self.tr("Project Settings"))
|
||||
self.aPrjSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog())
|
||||
self.mSettings.addAction(self.aPrjSettings)
|
||||
|
||||
self.aPreferences = QAction(self.tr("Preferences"))
|
||||
self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog())
|
||||
self.mSettings.addAction(self.aPreferences)
|
||||
self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
|
||||
self.mSettings.addAction(self.mainGui.mainMenu.aProjectSettings)
|
||||
self.mSettings.addSeparator()
|
||||
self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
|
||||
|
||||
self.tbSettings = QToolButton(self)
|
||||
self.tbSettings.setFont(lblFont)
|
||||
self.tbSettings.setText(self.tr("Settings"))
|
||||
self.tbSettings.setIcon(self.mainTheme.getIcon("settings"))
|
||||
self.tbSettings.setMenu(self.mSettings)
|
||||
self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||
self.tbSettings.setPopupMode(QToolButton.InstantPopup)
|
||||
@@ -132,8 +121,25 @@ class GuiViewsBar(QToolBar):
|
||||
self.addAction(self.aStats)
|
||||
self.addWidget(self.tbSettings)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiViewsBar initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self):
|
||||
"""Initialise GUI elements that depend on specific settings.
|
||||
"""
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
|
||||
self.aProject.setIcon(self.mainTheme.getIcon("view_editor"))
|
||||
self.aNovel.setIcon(self.mainTheme.getIcon("view_novel"))
|
||||
self.aOutline.setIcon(self.mainTheme.getIcon("view_outline"))
|
||||
self.aBuild.setIcon(self.mainTheme.getIcon("view_build"))
|
||||
self.aDetails.setIcon(self.mainTheme.getIcon("proj_details"))
|
||||
self.aStats.setIcon(self.mainTheme.getIcon("proj_stats"))
|
||||
self.tbSettings.setIcon(self.mainTheme.getIcon("settings"))
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiViewsBar
|
||||
|
||||
@@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from enum import Enum
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
|
||||
@@ -44,17 +44,18 @@ from novelwriter.gui import (
|
||||
GuiViewsBar
|
||||
)
|
||||
from novelwriter.dialogs import (
|
||||
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
|
||||
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
|
||||
GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
|
||||
GuiProjectSettings, GuiUpdates, GuiWordList
|
||||
)
|
||||
from novelwriter.tools import (
|
||||
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
|
||||
)
|
||||
from novelwriter.core import NWProject
|
||||
from novelwriter.core import NWProject, ProjectBuilder
|
||||
from novelwriter.enum import (
|
||||
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
|
||||
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
|
||||
)
|
||||
from novelwriter.common import getGuiItem, hexToInt
|
||||
from novelwriter.constants import nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,7 +63,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiMain(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
QMainWindow.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
logger.debug("Initialising GUI ...")
|
||||
self.setObjectName("GuiMain")
|
||||
@@ -78,7 +79,7 @@ class GuiMain(QMainWindow):
|
||||
logger.info("Qt5: %s (%d)", self.mainConf.verQtString, self.mainConf.verQtValue)
|
||||
logger.info("PyQt5: %s (%d)", self.mainConf.verPyQtString, self.mainConf.verPyQtValue)
|
||||
logger.info("Python: %s (0x%x)", self.mainConf.verPyString, self.mainConf.verPyHexVal)
|
||||
logger.info("GUI Language: %s", self.mainConf.guiLang)
|
||||
logger.info("GUI Language: %s", self.mainConf.guiLocale)
|
||||
|
||||
# Core Classes
|
||||
# ============
|
||||
@@ -92,9 +93,13 @@ class GuiMain(QMainWindow):
|
||||
self.idleTime = 0.0
|
||||
|
||||
# Prepare Main Window
|
||||
self.resize(*self.mainConf.getWinSize())
|
||||
self.resize(*self.mainConf.mainWinSize)
|
||||
self._updateWindowTitle()
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
|
||||
nwIcon = self.mainConf.assetPath("icons") / "novelwriter.svg"
|
||||
self.nwIcon = QIcon(str(nwIcon)) if nwIcon.is_file() else QIcon()
|
||||
self.setWindowIcon(self.nwIcon)
|
||||
qApp.setWindowIcon(self.nwIcon)
|
||||
|
||||
# Build the GUI
|
||||
# =============
|
||||
@@ -104,7 +109,7 @@ class GuiMain(QMainWindow):
|
||||
hWd = self.mainConf.pxInt(4)
|
||||
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
self.mainStatus = GuiMainStatus(self)
|
||||
self.projView = GuiProjectView(self)
|
||||
self.novelView = GuiNovelView(self)
|
||||
self.docEditor = GuiDocEditor(self)
|
||||
@@ -135,7 +140,7 @@ class GuiMain(QMainWindow):
|
||||
self.splitView.addWidget(self.docViewer)
|
||||
self.splitView.addWidget(self.viewMeta)
|
||||
self.splitView.setHandleWidth(hWd)
|
||||
self.splitView.setSizes(self.mainConf.getViewPanePos())
|
||||
self.splitView.setSizes(self.mainConf.viewPanePos)
|
||||
|
||||
# Splitter : Document Editor / Document Viewer
|
||||
self.splitDocs = QSplitter(Qt.Horizontal)
|
||||
@@ -149,7 +154,7 @@ class GuiMain(QMainWindow):
|
||||
self.splitMain.addWidget(self.treePane)
|
||||
self.splitMain.addWidget(self.splitDocs)
|
||||
self.splitMain.setHandleWidth(hWd)
|
||||
self.splitMain.setSizes(self.mainConf.getMainPanePos())
|
||||
self.splitMain.setSizes(self.mainConf.mainPanePos)
|
||||
|
||||
# Main Stack : Editor / Outline
|
||||
self.mainStack = QStackedWidget()
|
||||
@@ -189,29 +194,33 @@ class GuiMain(QMainWindow):
|
||||
# Set Main Window Elements
|
||||
self.setMenuBar(self.mainMenu)
|
||||
self.setCentralWidget(self.mainStack)
|
||||
self.setStatusBar(self.statusBar)
|
||||
self.setStatusBar(self.mainStatus)
|
||||
self.addToolBar(Qt.LeftToolBarArea, self.viewsBar)
|
||||
self.setContextMenuPolicy(Qt.NoContextMenu) # Issue #1147
|
||||
|
||||
# Connect Signals
|
||||
# ===============
|
||||
|
||||
self.theProject.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus)
|
||||
|
||||
self.viewsBar.viewChangeRequested.connect(self._changeView)
|
||||
|
||||
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
|
||||
self.projView.openDocumentRequest.connect(self._openDocument)
|
||||
self.projView.novelItemChanged.connect(self._treeNovelItemChanged)
|
||||
self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
|
||||
self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
|
||||
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
|
||||
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
|
||||
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
|
||||
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
|
||||
self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
|
||||
self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog)
|
||||
|
||||
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
|
||||
self.novelView.openDocumentRequest.connect(self._openDocument)
|
||||
|
||||
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
|
||||
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
|
||||
self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage)
|
||||
self.docEditor.docEditedStatusChanged.connect(self.mainStatus.doUpdateDocumentStatus)
|
||||
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
|
||||
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
|
||||
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
|
||||
@@ -253,7 +262,7 @@ class GuiMain(QMainWindow):
|
||||
keyEscape.activated.connect(self._keyPressEscape)
|
||||
|
||||
# Forward Functions
|
||||
self.setStatus = self.statusBar.setStatus
|
||||
self.setStatus = self.mainStatus.setStatus
|
||||
|
||||
# Force a show of the GUI
|
||||
self.show()
|
||||
@@ -265,7 +274,7 @@ class GuiMain(QMainWindow):
|
||||
self.initMain()
|
||||
self.asProjTimer.start()
|
||||
self.asDocTimer.start()
|
||||
self.statusBar.clearStatus()
|
||||
self.mainStatus.clearStatus()
|
||||
|
||||
# Handle Windows Mode
|
||||
self.showNormal()
|
||||
@@ -281,11 +290,6 @@ class GuiMain(QMainWindow):
|
||||
"and make sure you take regular backups."
|
||||
), nwAlert.WARN)
|
||||
|
||||
# If a project path was provided at command line, open it
|
||||
if self.mainConf.cmdOpen is not None:
|
||||
logger.debug("Opening project from additional command line option")
|
||||
self.openProject(self.mainConf.cmdOpen)
|
||||
|
||||
logger.info("novelWriter is ready ...")
|
||||
self.setStatus(self.tr("novelWriter is ready ..."))
|
||||
|
||||
@@ -303,10 +307,10 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.clearEditor()
|
||||
self.docEditor.setDictionaries()
|
||||
self.closeDocViewer()
|
||||
self.outlineView.clearOutline()
|
||||
self.outlineView.clearProject()
|
||||
|
||||
# General
|
||||
self.statusBar.clearStatus()
|
||||
self.mainStatus.clearStatus()
|
||||
self._updateWindowTitle()
|
||||
|
||||
return True
|
||||
@@ -318,13 +322,22 @@ class GuiMain(QMainWindow):
|
||||
self.asDocTimer.setInterval(int(self.mainConf.autoSaveDoc*1000))
|
||||
return True
|
||||
|
||||
def releaseNotes(self):
|
||||
"""Determine whether release notes need to be shown, and show
|
||||
them by calling the About dialog.
|
||||
def postLaunchTasks(self, cmdOpen):
|
||||
"""This function is called after the main window is created to
|
||||
determine what to open or show after initialisation.
|
||||
"""
|
||||
if cmdOpen:
|
||||
logger.info("Command line path: %s", cmdOpen)
|
||||
self.openProject(cmdOpen)
|
||||
|
||||
if not self.hasProject:
|
||||
self.showProjectLoadDialog()
|
||||
|
||||
# Determine whether release notes need to be shown or not
|
||||
if hexToInt(self.mainConf.lastNotes) < hexToInt(novelwriter.__hexversion__):
|
||||
self.mainConf.lastNotes = novelwriter.__hexversion__
|
||||
self.showAboutNWDialog(showNotes=True)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -352,7 +365,7 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No projData or projPath set")
|
||||
return False
|
||||
|
||||
if os.path.isfile(os.path.join(projPath, self.theProject.projFile)):
|
||||
if (Path(projPath) / nwFiles.PROJ_FILE).is_file():
|
||||
self.makeAlert(self.tr(
|
||||
"A project already exists in that location. "
|
||||
"Please choose another folder."
|
||||
@@ -360,21 +373,10 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
logger.info("Creating new project")
|
||||
if self.theProject.newProject(projData):
|
||||
self.hasProject = True
|
||||
self.rebuildTrees()
|
||||
self.saveProject()
|
||||
self.docEditor.setDictionaries()
|
||||
self.outlineView.updateRootItem(None)
|
||||
self.novelView.openProjectTasks()
|
||||
self.rebuildIndex(beQuiet=True)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.statusBar.setProjectStatus(nwState.GOOD)
|
||||
self.statusBar.setDocumentStatus(nwState.NONE)
|
||||
self.statusBar.setStatus(self.tr("New project created ..."))
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
nwProject = ProjectBuilder(self)
|
||||
if nwProject.buildProject(projData):
|
||||
self.openProject(projPath)
|
||||
else:
|
||||
self.theProject.clearProject()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -402,34 +404,31 @@ class GuiMain(QMainWindow):
|
||||
if self.docEditor.docChanged():
|
||||
self.saveDocument()
|
||||
|
||||
if self.theProject.projAltered:
|
||||
saveOK = self.saveProject()
|
||||
doBackup = False
|
||||
if self.theProject.doBackup and self.mainConf.backupOnClose:
|
||||
doBackup = True
|
||||
if self.mainConf.askBeforeBackup:
|
||||
msgYes = self.askQuestion(
|
||||
self.tr("Backup Project"),
|
||||
self.tr("Backup the current project?")
|
||||
)
|
||||
if not msgYes:
|
||||
doBackup = False
|
||||
if doBackup:
|
||||
self.theProject.zipIt(False)
|
||||
else:
|
||||
saveOK = True
|
||||
saveOK = self.saveProject()
|
||||
doBackup = False
|
||||
if self.theProject.data.doBackup and self.mainConf.backupOnClose:
|
||||
doBackup = True
|
||||
if self.mainConf.askBeforeBackup:
|
||||
msgYes = self.askQuestion(
|
||||
self.tr("Backup Project"),
|
||||
self.tr("Backup the current project?")
|
||||
)
|
||||
if not msgYes:
|
||||
doBackup = False
|
||||
|
||||
if doBackup:
|
||||
self.theProject.backupProject(False)
|
||||
|
||||
if saveOK:
|
||||
self.closeDocument()
|
||||
self.docViewer.clearNavHistory()
|
||||
self.outlineView.closeOutline()
|
||||
self.outlineView.closeProjectTasks()
|
||||
self.novelView.closeProjectTasks()
|
||||
|
||||
self.theProject.closeProject(self.idleTime)
|
||||
self.idleRefTime = time()
|
||||
self.idleTime = 0.0
|
||||
|
||||
self.theProject.index.clearIndex()
|
||||
self.clearGUI()
|
||||
self.hasProject = False
|
||||
self._changeView(nwView.PROJECT)
|
||||
@@ -454,7 +453,8 @@ class GuiMain(QMainWindow):
|
||||
if not self.theProject.openProject(projFile):
|
||||
# The project open failed.
|
||||
|
||||
if self.theProject.lockedBy is None:
|
||||
lockStatus = self.theProject.getLockStatus()
|
||||
if lockStatus is None:
|
||||
# The project is not locked, so failed for some other
|
||||
# reason handled by the project class.
|
||||
return False
|
||||
@@ -466,10 +466,8 @@ class GuiMain(QMainWindow):
|
||||
"'{0}' ({1} {2}), last active on {3}."
|
||||
)
|
||||
).format(
|
||||
self.theProject.lockedBy[0],
|
||||
self.theProject.lockedBy[1],
|
||||
self.theProject.lockedBy[2],
|
||||
datetime.fromtimestamp(int(self.theProject.lockedBy[3])).strftime("%x %X")
|
||||
lockStatus[0], lockStatus[1], lockStatus[2],
|
||||
datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X")
|
||||
)
|
||||
except Exception:
|
||||
lockDetails = ""
|
||||
@@ -505,25 +503,32 @@ class GuiMain(QMainWindow):
|
||||
self.idleRefTime = time()
|
||||
self.idleTime = 0.0
|
||||
|
||||
# Load the tag index
|
||||
self.theProject.index.loadIndex()
|
||||
|
||||
# Update GUI
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
self._updateWindowTitle(self.theProject.data.name)
|
||||
self.rebuildTrees()
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.outlineView.updateRootItem(None)
|
||||
self.docEditor.toggleSpellCheck(self.theProject.data.spellCheck)
|
||||
self.mainStatus.setRefTime(self.theProject.projOpened)
|
||||
self.projView.openProjectTasks()
|
||||
self.novelView.openProjectTasks()
|
||||
self.outlineView.openProjectTasks()
|
||||
self._updateStatusWordCount()
|
||||
|
||||
# Restore previously open documents, if any
|
||||
if self.theProject.lastEdited is not None:
|
||||
self.openDocument(self.theProject.lastEdited, doScroll=True)
|
||||
# If none was recorded, open the first document found
|
||||
lastEdited = self.theProject.data.getLastHandle("editor")
|
||||
if lastEdited is None:
|
||||
for nwItem in self.theProject.tree:
|
||||
if nwItem and nwItem.isFileType():
|
||||
lastEdited = nwItem.itemHandle
|
||||
break
|
||||
|
||||
if self.theProject.lastViewed is not None:
|
||||
self.viewDocument(self.theProject.lastViewed)
|
||||
if lastEdited is not None:
|
||||
self.openDocument(lastEdited, doScroll=True)
|
||||
|
||||
lastViewed = self.theProject.data.getLastHandle("viewer")
|
||||
if lastViewed is not None:
|
||||
self.viewDocument(lastViewed)
|
||||
|
||||
# Check if we need to rebuild the index
|
||||
if self.theProject.index.indexBroken:
|
||||
@@ -548,9 +553,8 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
self.projView.saveProjectTree()
|
||||
if self.theProject.saveProject(autoSave=autoSave):
|
||||
self.theProject.index.saveIndex()
|
||||
self.projView.saveProjectTasks()
|
||||
self.theProject.saveProject(autoSave=autoSave)
|
||||
|
||||
return True
|
||||
|
||||
@@ -558,7 +562,7 @@ class GuiMain(QMainWindow):
|
||||
# Document Actions
|
||||
##
|
||||
|
||||
def closeDocument(self):
|
||||
def closeDocument(self, beforeOpen=False):
|
||||
"""Close the document and clear the editor and title field.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
@@ -573,6 +577,8 @@ class GuiMain(QMainWindow):
|
||||
if self.docEditor.docChanged():
|
||||
self.saveDocument()
|
||||
self.docEditor.clearEditor()
|
||||
if not beforeOpen:
|
||||
self.novelView.setActiveHandle(None)
|
||||
|
||||
return True
|
||||
|
||||
@@ -587,13 +593,14 @@ class GuiMain(QMainWindow):
|
||||
logger.debug("Requested item '%s' is not a document", tHandle)
|
||||
return False
|
||||
|
||||
self.closeDocument()
|
||||
self.closeDocument(beforeOpen=True)
|
||||
self._changeView(nwView.EDITOR)
|
||||
if self.docEditor.loadText(tHandle, tLine):
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
self.theProject.setLastEdited(tHandle)
|
||||
self.theProject.data.setLastHandle(tHandle, "editor")
|
||||
self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
|
||||
self.novelView.setActiveHandle(tHandle)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -611,7 +618,7 @@ class GuiMain(QMainWindow):
|
||||
fHandle = None # The first file handle we encounter
|
||||
foundIt = False # We've found tHandle, pick the next we see
|
||||
for tItem in self.theProject.tree:
|
||||
if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE):
|
||||
if tItem is None or not tItem.isFileType():
|
||||
continue
|
||||
if fHandle is None:
|
||||
fHandle = tItem.itemHandle
|
||||
@@ -652,21 +659,18 @@ class GuiMain(QMainWindow):
|
||||
logger.debug("Viewing document, but no handle provided")
|
||||
|
||||
if self.docEditor.hasFocus():
|
||||
logger.verbose("Trying editor document")
|
||||
tHandle = self.docEditor.docHandle()
|
||||
|
||||
if tHandle is not None:
|
||||
self.saveDocument()
|
||||
else:
|
||||
logger.verbose("Trying selected document")
|
||||
tHandle = self.projView.getSelectedHandle()
|
||||
|
||||
if tHandle is None:
|
||||
logger.verbose("Trying last viewed document")
|
||||
tHandle = self.theProject.lastViewed
|
||||
tHandle = self.theProject.data.getLastHandle("viewer")
|
||||
|
||||
if tHandle is None:
|
||||
logger.verbose("No document to view, giving up")
|
||||
logger.debug("No document to view, giving up")
|
||||
return False
|
||||
|
||||
# Make sure main tab is in Editor view
|
||||
@@ -695,7 +699,7 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
lastPath = self.mainConf.lastPath
|
||||
lastPath = self.mainConf.lastPath()
|
||||
extFilter = [
|
||||
self.tr("Text files ({0})").format("*.txt"),
|
||||
self.tr("Markdown files ({0})").format("*.md"),
|
||||
@@ -703,7 +707,7 @@ class GuiMain(QMainWindow):
|
||||
self.tr("All files ({0})").format("*"),
|
||||
]
|
||||
loadFile, _ = QFileDialog.getOpenFileName(
|
||||
self, self.tr("Import File"), lastPath, filter=";;".join(extFilter)
|
||||
self, self.tr("Import File"), str(lastPath), filter=";;".join(extFilter)
|
||||
)
|
||||
if not loadFile:
|
||||
return False
|
||||
@@ -743,30 +747,6 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def mergeDocuments(self):
|
||||
"""Merge multiple documents to one single new document.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
dlgMerge = GuiDocMerge(self)
|
||||
dlgMerge.exec_()
|
||||
|
||||
return True
|
||||
|
||||
def splitDocument(self):
|
||||
"""Split a single document into multiple documents.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
dlgSplit = GuiDocSplit(self)
|
||||
dlgSplit.exec_()
|
||||
|
||||
return True
|
||||
|
||||
def passDocumentAction(self, theAction):
|
||||
"""Pass on document action to the document viewer if it has
|
||||
focus, or pass it to the document editor if it or any of
|
||||
@@ -818,15 +798,11 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
if tHandle is None:
|
||||
if self.docEditor.anyFocus() or self.isFocusMode:
|
||||
tHandle = self.docEditor.docHandle()
|
||||
else:
|
||||
tHandle = self.projView.getSelectedHandle()
|
||||
if tHandle:
|
||||
return self.projView.renameTreeItem(tHandle)
|
||||
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode):
|
||||
tHandle = self.docEditor.docHandle()
|
||||
self.projView.renameTreeItem(tHandle)
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
def rebuildTrees(self):
|
||||
"""Rebuild the project tree.
|
||||
@@ -854,18 +830,9 @@ class GuiMain(QMainWindow):
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
tStart = time()
|
||||
|
||||
self.projView.saveProjectTree()
|
||||
self.theProject.index.clearIndex()
|
||||
|
||||
for tItem in self.theProject.tree:
|
||||
if tItem is None: # pragma: no cover
|
||||
continue # This is a bug trap
|
||||
|
||||
logger.verbose("Indexing '%s'", tItem.itemName)
|
||||
if self.theProject.index.reIndexHandle(tItem.itemHandle):
|
||||
# Update Word Counts
|
||||
self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
|
||||
self.projView.setTreeItemValues(tItem.itemHandle)
|
||||
self.projView.saveProjectTasks()
|
||||
self.theProject.index.rebuildIndex()
|
||||
self.projView.populateTree()
|
||||
|
||||
tEnd = time()
|
||||
self.setStatus(
|
||||
@@ -894,6 +861,7 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
dlgProj = GuiProjectLoad(self)
|
||||
dlgProj.exec_()
|
||||
|
||||
if dlgProj.result() == QDialog.Accepted:
|
||||
if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
|
||||
self.openProject(dlgProj.openPath)
|
||||
@@ -922,25 +890,52 @@ class GuiMain(QMainWindow):
|
||||
if dlgConf.result() == QDialog.Accepted:
|
||||
logger.debug("Applying new preferences")
|
||||
self.initMain()
|
||||
self.mainTheme.updateTheme()
|
||||
self.saveDocument()
|
||||
|
||||
if dlgConf.needsRestart:
|
||||
self.makeAlert(self.tr(
|
||||
"Some changes will not be applied until novelWriter has been restarted."
|
||||
), nwAlert.INFO)
|
||||
|
||||
if dlgConf.refreshTree:
|
||||
self.projView.populateTree()
|
||||
|
||||
if dlgConf.updateTheme:
|
||||
# We are doing this manually instead of connecting to
|
||||
# qApp.paletteChanged since the processing order matters
|
||||
self.mainTheme.loadTheme()
|
||||
self.docEditor.updateTheme()
|
||||
self.docViewer.updateTheme()
|
||||
self.viewsBar.updateTheme()
|
||||
self.projView.updateTheme()
|
||||
self.novelView.updateTheme()
|
||||
self.outlineView.updateTheme()
|
||||
self.itemDetails.updateTheme()
|
||||
self.mainStatus.updateTheme()
|
||||
|
||||
if dlgConf.updateSyntax:
|
||||
self.mainTheme.loadSyntax()
|
||||
self.docEditor.updateSyntaxColours()
|
||||
|
||||
self.docEditor.initEditor()
|
||||
self.docViewer.initViewer()
|
||||
self.projView.initSettings()
|
||||
self.novelView.initSettings()
|
||||
self.outlineView.initOutline()
|
||||
self.outlineView.initSettings()
|
||||
|
||||
self._updateStatusWordCount()
|
||||
|
||||
return
|
||||
|
||||
def showProjectSettingsDialog(self):
|
||||
@pyqtSlot(int)
|
||||
def showProjectSettingsDialog(self, focusTab=GuiProjectSettings.TAB_MAIN):
|
||||
"""Open the project settings dialog.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
dlgProj = GuiProjectSettings(self)
|
||||
dlgProj = GuiProjectSettings(self, focusTab=focusTab)
|
||||
dlgProj.exec_()
|
||||
|
||||
if dlgProj.result() == QDialog.Accepted:
|
||||
@@ -948,7 +943,7 @@ class GuiMain(QMainWindow):
|
||||
if dlgProj.spellChanged:
|
||||
self.docEditor.setDictionaries()
|
||||
self.itemDetails.refreshDetails()
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
self._updateWindowTitle(self.theProject.data.name)
|
||||
|
||||
return True
|
||||
|
||||
@@ -962,6 +957,7 @@ class GuiMain(QMainWindow):
|
||||
dlgDetails = getGuiItem("GuiProjectDetails")
|
||||
if dlgDetails is None:
|
||||
dlgDetails = GuiProjectDetails(self)
|
||||
assert isinstance(dlgDetails, GuiProjectDetails)
|
||||
|
||||
dlgDetails.setModal(False)
|
||||
dlgDetails.show()
|
||||
@@ -980,6 +976,7 @@ class GuiMain(QMainWindow):
|
||||
dlgBuild = getGuiItem("GuiBuildNovel")
|
||||
if dlgBuild is None:
|
||||
dlgBuild = GuiBuildNovel(self)
|
||||
assert isinstance(dlgBuild, GuiBuildNovel)
|
||||
|
||||
dlgBuild.setModal(False)
|
||||
dlgBuild.show()
|
||||
@@ -999,6 +996,7 @@ class GuiMain(QMainWindow):
|
||||
dlgLipsum = getGuiItem("GuiLipsum")
|
||||
if dlgLipsum is None:
|
||||
dlgLipsum = GuiLipsum(self)
|
||||
assert isinstance(dlgLipsum, GuiLipsum)
|
||||
|
||||
dlgLipsum.setModal(False)
|
||||
dlgLipsum.show()
|
||||
@@ -1033,6 +1031,7 @@ class GuiMain(QMainWindow):
|
||||
dlgStats = getGuiItem("GuiWritingStats")
|
||||
if dlgStats is None:
|
||||
dlgStats = GuiWritingStats(self)
|
||||
assert isinstance(dlgStats, GuiWritingStats)
|
||||
|
||||
dlgStats.setModal(False)
|
||||
dlgStats.show()
|
||||
@@ -1048,6 +1047,7 @@ class GuiMain(QMainWindow):
|
||||
dlgAbout = getGuiItem("GuiAbout")
|
||||
if dlgAbout is None:
|
||||
dlgAbout = GuiAbout(self)
|
||||
assert isinstance(dlgAbout, GuiAbout)
|
||||
|
||||
dlgAbout.setModal(True)
|
||||
dlgAbout.show()
|
||||
@@ -1073,6 +1073,7 @@ class GuiMain(QMainWindow):
|
||||
dlgUpdate = getGuiItem("GuiUpdates")
|
||||
if dlgUpdate is None:
|
||||
dlgUpdate = GuiUpdates(self)
|
||||
assert isinstance(dlgUpdate, GuiUpdates)
|
||||
|
||||
dlgUpdate.setModal(True)
|
||||
dlgUpdate.show()
|
||||
@@ -1136,7 +1137,7 @@ class GuiMain(QMainWindow):
|
||||
errors since it is initialised before the GUI itself.
|
||||
"""
|
||||
if self.mainConf.hasError:
|
||||
self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR)
|
||||
self.makeAlert(self.mainConf.errorText(), nwAlert.ERROR)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1162,14 +1163,13 @@ class GuiMain(QMainWindow):
|
||||
|
||||
if not self.isFocusMode:
|
||||
self.mainConf.setMainPanePos(self.splitMain.sizes())
|
||||
self.mainConf.setDocPanePos(self.splitDocs.sizes())
|
||||
self.mainConf.setOutlinePanePos(self.outlineView.splitSizes())
|
||||
if self.viewMeta.isVisible():
|
||||
self.mainConf.setViewPanePos(self.splitView.sizes())
|
||||
|
||||
self.mainConf.setShowRefPanel(self.viewMeta.isVisible())
|
||||
self.mainConf.showRefPanel = self.viewMeta.isVisible()
|
||||
if not self.mainConf.isFullScreen:
|
||||
self.mainConf.setWinSize(self.width(), self.height())
|
||||
self.mainConf.setMainWinSize(self.width(), self.height())
|
||||
|
||||
if self.hasProject:
|
||||
self.closeProject(True)
|
||||
@@ -1189,7 +1189,7 @@ class GuiMain(QMainWindow):
|
||||
if tabIdx == self.idxProjView:
|
||||
self.projView.setFocus()
|
||||
elif tabIdx == self.idxNovelView:
|
||||
self.novelView.setFocus()
|
||||
self.novelView.setTreeFocus()
|
||||
elif paneNo == nwWidget.EDITOR:
|
||||
self._changeView(nwView.EDITOR)
|
||||
self.docEditor.setFocus()
|
||||
@@ -1205,14 +1205,14 @@ class GuiMain(QMainWindow):
|
||||
"""Close the document edit panel. This does not hide the editor.
|
||||
"""
|
||||
self.closeDocument()
|
||||
self.theProject.setLastEdited(None)
|
||||
self.theProject.data.setLastHandle(None, "editor")
|
||||
return
|
||||
|
||||
def closeDocViewer(self):
|
||||
"""Close the document view panel.
|
||||
"""
|
||||
self.docViewer.clearViewer()
|
||||
self.theProject.setLastViewed(None)
|
||||
self.theProject.data.setLastHandle(None, "viewer")
|
||||
bPos = self.splitMain.sizes()
|
||||
self.splitView.setVisible(False)
|
||||
self.splitDocs.setSizes([bPos[1], 0])
|
||||
@@ -1223,11 +1223,9 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
if self.docEditor.docHandle() is None:
|
||||
logger.error("No document open, so not activating Focus Mode")
|
||||
self.mainMenu.setFocusMode(self.isFocusMode)
|
||||
return False
|
||||
|
||||
self.isFocusMode = not self.isFocusMode
|
||||
self.mainMenu.setFocusMode(self.isFocusMode)
|
||||
if self.isFocusMode:
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.switchFocus(nwWidget.EDITOR)
|
||||
@@ -1236,7 +1234,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
isVisible = not self.isFocusMode
|
||||
self.treePane.setVisible(isVisible)
|
||||
self.statusBar.setVisible(isVisible)
|
||||
self.mainStatus.setVisible(isVisible)
|
||||
self.mainMenu.setVisible(isVisible)
|
||||
self.viewsBar.setVisible(isVisible)
|
||||
|
||||
@@ -1324,6 +1322,7 @@ class GuiMain(QMainWindow):
|
||||
self.addAction(self.mainMenu.aInsMinus)
|
||||
self.addAction(self.mainMenu.aInsTimes)
|
||||
self.addAction(self.mainMenu.aInsDivide)
|
||||
self.addAction(self.mainMenu.aInsSynopsis)
|
||||
|
||||
for mAction, _ in self.mainMenu.mInsKWItems.values():
|
||||
self.addAction(mAction)
|
||||
@@ -1360,7 +1359,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Help
|
||||
self.addAction(self.mainMenu.aHelpDocs)
|
||||
if self.mainConf.pdfDocs is not None:
|
||||
if isinstance(self.mainConf.pdfDocs, Path):
|
||||
self.addAction(self.mainMenu.aPdfDocs)
|
||||
|
||||
return True
|
||||
@@ -1379,7 +1378,7 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
doSave = self.hasProject
|
||||
doSave &= self.theProject.projChanged
|
||||
doSave &= self.theProject.projPath is not None
|
||||
doSave &= self.theProject.storage.isOpen()
|
||||
|
||||
if doSave:
|
||||
logger.debug("Autosaving project")
|
||||
@@ -1520,12 +1519,12 @@ class GuiMain(QMainWindow):
|
||||
|
||||
if editIdle or userIdle:
|
||||
self.idleTime += currTime - self.idleRefTime
|
||||
self.statusBar.setUserIdle(True)
|
||||
self.mainStatus.setUserIdle(True)
|
||||
else:
|
||||
self.statusBar.setUserIdle(False)
|
||||
self.mainStatus.setUserIdle(False)
|
||||
|
||||
self.idleRefTime = currTime
|
||||
self.statusBar.updateTime(idleTime=self.idleTime)
|
||||
self.mainStatus.updateTime(idleTime=self.idleTime)
|
||||
|
||||
return
|
||||
|
||||
@@ -1534,30 +1533,17 @@ class GuiMain(QMainWindow):
|
||||
"""Update the word count on the status bar.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
self.statusBar.setProjectStats(0, 0)
|
||||
self.mainStatus.setProjectStats(0, 0)
|
||||
|
||||
logger.verbose("Updating total word count")
|
||||
self.theProject.updateWordCounts()
|
||||
if self.mainConf.incNotesWCount:
|
||||
currWords = self.theProject.currWCount
|
||||
diffWords = currWords - self.theProject.lastWCount
|
||||
iTotal = sum(self.theProject.data.initCounts)
|
||||
cTotal = sum(self.theProject.data.currCounts)
|
||||
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
|
||||
else:
|
||||
currWords = self.theProject.currNovelWC
|
||||
diffWords = currWords - self.theProject.lastNovelWC
|
||||
|
||||
self.statusBar.setProjectStats(currWords, diffWords)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeNovelItemChanged(self):
|
||||
"""Triggered when there is a change to a novel item in the
|
||||
project tree.
|
||||
"""
|
||||
if self.mainStack.currentIndex() == self.idxOutlineView:
|
||||
logger.verbose("Novel tree changed while Outline tab active")
|
||||
if self.hasProject:
|
||||
self.outlineView.refreshView(novelChanged=True)
|
||||
iNovel, _ = self.theProject.data.initCounts
|
||||
cNovel, _ = self.theProject.data.currCounts
|
||||
self.mainStatus.setProjectStats(cNovel, cNovel - iNovel)
|
||||
|
||||
return
|
||||
|
||||
@@ -1585,13 +1571,9 @@ class GuiMain(QMainWindow):
|
||||
def _mainStackChanged(self, stIndex):
|
||||
"""Activated when the main window tab is changed.
|
||||
"""
|
||||
if stIndex == self.idxEditorView:
|
||||
logger.verbose("Editor View activated")
|
||||
elif stIndex == self.idxOutlineView:
|
||||
logger.verbose("Outline View activated")
|
||||
if stIndex == self.idxOutlineView:
|
||||
if self.hasProject:
|
||||
self.outlineView.refreshView()
|
||||
|
||||
self.outlineView.refreshTree()
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
@@ -1601,11 +1583,9 @@ class GuiMain(QMainWindow):
|
||||
sHandle = None
|
||||
|
||||
if stIndex == self.idxProjView:
|
||||
logger.verbose("Project Tree View activated")
|
||||
sHandle = self.projView.getSelectedHandle()
|
||||
|
||||
elif stIndex == self.idxNovelView:
|
||||
logger.verbose("Novel Tree View activated")
|
||||
if self.hasProject:
|
||||
self.novelView.refreshTree()
|
||||
sHandle, _ = self.novelView.getSelectedHandle()
|
||||
|
||||
@@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtGui import (
|
||||
@@ -47,8 +47,8 @@ from novelwriter.core import ToHtml, ToOdt, ToMarkdown
|
||||
from novelwriter.enum import nwAlert, nwItemType, nwItemLayout, nwItemClass
|
||||
from novelwriter.error import formatException, logException
|
||||
from novelwriter.common import fuzzyTime, makeFileNameSafe
|
||||
from novelwriter.custom import QSwitch
|
||||
from novelwriter.constants import nwConst, nwFiles
|
||||
from novelwriter.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,7 +66,7 @@ class GuiBuildNovel(QDialog):
|
||||
FMT_JSON_M = 9 # nW Markdown wrapped in JSON
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiBuildNovel ...")
|
||||
self.setObjectName("GuiBuildNovel")
|
||||
@@ -126,7 +126,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.fmtTitle.setMinimumWidth(xFmt)
|
||||
self.fmtTitle.setToolTip(fmtHelp)
|
||||
self.fmtTitle.setText(
|
||||
self._reFmtCodes(self.theProject.titleFormat["title"])
|
||||
self._reFmtCodes(self.theProject.data.getTitleFormat("title"))
|
||||
)
|
||||
|
||||
self.fmtChapter = QLineEdit()
|
||||
@@ -134,7 +134,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.fmtChapter.setMinimumWidth(xFmt)
|
||||
self.fmtChapter.setToolTip(fmtHelp)
|
||||
self.fmtChapter.setText(
|
||||
self._reFmtCodes(self.theProject.titleFormat["chapter"])
|
||||
self._reFmtCodes(self.theProject.data.getTitleFormat("chapter"))
|
||||
)
|
||||
|
||||
self.fmtUnnumbered = QLineEdit()
|
||||
@@ -142,7 +142,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.fmtUnnumbered.setMinimumWidth(xFmt)
|
||||
self.fmtUnnumbered.setToolTip(fmtHelp)
|
||||
self.fmtUnnumbered.setText(
|
||||
self._reFmtCodes(self.theProject.titleFormat["unnumbered"])
|
||||
self._reFmtCodes(self.theProject.data.getTitleFormat("unnumbered"))
|
||||
)
|
||||
|
||||
self.fmtScene = QLineEdit()
|
||||
@@ -150,7 +150,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.fmtScene.setMinimumWidth(xFmt)
|
||||
self.fmtScene.setToolTip(fmtHelp + fmtScHelp)
|
||||
self.fmtScene.setText(
|
||||
self._reFmtCodes(self.theProject.titleFormat["scene"])
|
||||
self._reFmtCodes(self.theProject.data.getTitleFormat("scene"))
|
||||
)
|
||||
|
||||
self.fmtSection = QLineEdit()
|
||||
@@ -158,7 +158,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.fmtSection.setMinimumWidth(xFmt)
|
||||
self.fmtSection.setToolTip(fmtHelp + fmtScHelp)
|
||||
self.fmtSection.setText(
|
||||
self._reFmtCodes(self.theProject.titleFormat["section"])
|
||||
self._reFmtCodes(self.theProject.data.getTitleFormat("section"))
|
||||
)
|
||||
|
||||
self.buildLang = QComboBox()
|
||||
@@ -168,7 +168,7 @@ class GuiBuildNovel(QDialog):
|
||||
for langID, langName in theLangs:
|
||||
self.buildLang.addItem(langName, langID)
|
||||
|
||||
langIdx = self.buildLang.findData(self.theProject.projLang)
|
||||
langIdx = self.buildLang.findData(self.theProject.data.language)
|
||||
if langIdx != -1:
|
||||
self.buildLang.setCurrentIndex(langIdx)
|
||||
|
||||
@@ -351,6 +351,36 @@ class GuiBuildNovel(QDialog):
|
||||
self.textForm.setColumnStretch(0, 1)
|
||||
self.textForm.setColumnStretch(1, 0)
|
||||
|
||||
# Root Filter Options
|
||||
# ===================
|
||||
|
||||
self.rootGroup = QGroupBox(self.tr("Root Filter Options"), self)
|
||||
self.rootForm = QGridLayout(self)
|
||||
self.rootGroup.setLayout(self.rootForm)
|
||||
|
||||
rootFilter = pOptions.getValue("GuiBuildNovel", "rootFilter", [])
|
||||
if not isinstance(rootFilter, list):
|
||||
rootFilter = []
|
||||
|
||||
iRow = 0
|
||||
self.rootSelection = {}
|
||||
for tHandle, nwItem in self.theProject.tree.iterRoots(None):
|
||||
if not nwItem.isInactive():
|
||||
rootLabel = QLabel(nwItem.itemName)
|
||||
rootLabel.setWordWrap(True)
|
||||
|
||||
rootValue = QSwitch(width=wS, height=hS)
|
||||
rootValue.setChecked(tHandle not in rootFilter)
|
||||
|
||||
self.rootSelection[tHandle] = rootValue
|
||||
self.rootForm.addWidget(rootLabel, iRow, 0, 1, 1, Qt.AlignLeft)
|
||||
self.rootForm.addWidget(rootValue, iRow, 1, 1, 1, Qt.AlignRight)
|
||||
|
||||
iRow += 1
|
||||
|
||||
self.rootForm.setColumnStretch(0, 1)
|
||||
self.rootForm.setColumnStretch(1, 0)
|
||||
|
||||
# File Filter Options
|
||||
# ===================
|
||||
|
||||
@@ -375,13 +405,13 @@ class GuiBuildNovel(QDialog):
|
||||
|
||||
novelLabel = QLabel(self.tr("Include novel files"))
|
||||
notesLabel = QLabel(self.tr("Include note files"))
|
||||
exportLabel = QLabel(self.tr("Ignore export flag"))
|
||||
activeLabel = QLabel(self.tr("Include inactive files"))
|
||||
|
||||
self.fileForm.addWidget(novelLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight)
|
||||
self.fileForm.addWidget(notesLabel, 1, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fileForm.addWidget(self.noteFiles, 1, 1, 1, 1, Qt.AlignRight)
|
||||
self.fileForm.addWidget(exportLabel, 2, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fileForm.addWidget(activeLabel, 2, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fileForm.addWidget(self.ignoreFlag, 2, 1, 1, 1, Qt.AlignRight)
|
||||
|
||||
self.fileForm.setColumnStretch(0, 1)
|
||||
@@ -503,6 +533,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.toolsBox.addWidget(self.fontGroup)
|
||||
self.toolsBox.addWidget(self.styleGroup)
|
||||
self.toolsBox.addWidget(self.textGroup)
|
||||
self.toolsBox.addWidget(self.rootGroup)
|
||||
self.toolsBox.addWidget(self.fileGroup)
|
||||
self.toolsBox.addWidget(self.exportGroup)
|
||||
self.toolsBox.addStretch(1)
|
||||
@@ -716,6 +747,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.buildProgress.setMaximum(len(self.theProject.tree))
|
||||
self.buildProgress.setValue(0)
|
||||
|
||||
rootFilter = set(self._generateRootFilter())
|
||||
for nItt, tItem in enumerate(self.theProject.tree):
|
||||
|
||||
noteRoot = noteFiles
|
||||
@@ -730,7 +762,7 @@ class GuiBuildNovel(QDialog):
|
||||
if doConvert:
|
||||
bldObj.doConvert()
|
||||
|
||||
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
|
||||
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag, rootFilter):
|
||||
bldObj.setText(tItem.itemHandle)
|
||||
bldObj.doPreProcessing()
|
||||
bldObj.tokenizeText()
|
||||
@@ -766,7 +798,7 @@ class GuiBuildNovel(QDialog):
|
||||
|
||||
return
|
||||
|
||||
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag):
|
||||
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag, rootFilter):
|
||||
"""This function checks whether a file should be included in the
|
||||
export or not. For standard note and novel files, this is
|
||||
controlled by the options selected by the user. For other files
|
||||
@@ -781,14 +813,17 @@ class GuiBuildNovel(QDialog):
|
||||
if theItem is None:
|
||||
return False
|
||||
|
||||
if not (theItem.isExported or ignoreFlag):
|
||||
if not (theItem.isActive or ignoreFlag):
|
||||
return False
|
||||
|
||||
isNone = theItem.itemType != nwItemType.FILE
|
||||
if theItem.itemRoot in rootFilter:
|
||||
return False
|
||||
|
||||
isNone = not theItem.isFileType()
|
||||
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
isNone |= theItem.isInactive()
|
||||
isNone |= theItem.itemParent is None
|
||||
isNote = theItem.itemLayout == nwItemLayout.NOTE
|
||||
isNote = theItem.isNoteLayout()
|
||||
isNovel = not isNone and not isNote
|
||||
|
||||
if isNone:
|
||||
@@ -853,15 +888,11 @@ class GuiBuildNovel(QDialog):
|
||||
# Generate File Name
|
||||
# ==================
|
||||
|
||||
cleanName = makeFileNameSafe(self.theProject.projName)
|
||||
cleanName = makeFileNameSafe(self.theProject.data.name)
|
||||
fileName = "%s.%s" % (cleanName, fileExt)
|
||||
saveDir = self.mainConf.lastPath
|
||||
if not os.path.isdir(saveDir):
|
||||
saveDir = os.path.expanduser("~")
|
||||
|
||||
savePath = os.path.join(saveDir, fileName)
|
||||
savePath = self.mainConf.lastPath() / fileName
|
||||
savePath, _ = QFileDialog.getSaveFileName(
|
||||
self, self.tr("Save Document As"), savePath
|
||||
self, self.tr("Save Document As"), str(savePath)
|
||||
)
|
||||
if not savePath:
|
||||
return False
|
||||
@@ -937,9 +968,9 @@ class GuiBuildNovel(QDialog):
|
||||
elif theFmt == self.FMT_JSON_H or theFmt == self.FMT_JSON_M:
|
||||
jsonData = {
|
||||
"meta": {
|
||||
"workingTitle": self.theProject.projName,
|
||||
"novelTitle": self.theProject.bookTitle,
|
||||
"authors": self.theProject.bookAuthors,
|
||||
"workingTitle": self.theProject.data.name,
|
||||
"novelTitle": self.theProject.data.title,
|
||||
"authors": self.theProject.data.authors,
|
||||
"buildTime": self.buildTime,
|
||||
}
|
||||
}
|
||||
@@ -1045,12 +1076,20 @@ class GuiBuildNovel(QDialog):
|
||||
|
||||
return
|
||||
|
||||
def _generateRootFilter(self):
|
||||
"""Return a list of all root folders that are filtered out.
|
||||
"""
|
||||
return [h for h, s in self.rootSelection.items() if not s.isChecked()]
|
||||
|
||||
def _loadCache(self):
|
||||
"""Save the current data to cache.
|
||||
"""
|
||||
buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
|
||||
buildCache = self.theProject.storage.getCacheFile(nwFiles.BUILD_CACHE)
|
||||
if not isinstance(buildCache, Path):
|
||||
return False
|
||||
|
||||
dataCount = 0
|
||||
if os.path.isfile(buildCache):
|
||||
if buildCache.exists():
|
||||
logger.debug("Loading build cache")
|
||||
try:
|
||||
with open(buildCache, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -1075,7 +1114,10 @@ class GuiBuildNovel(QDialog):
|
||||
def _saveCache(self):
|
||||
"""Save the current data to cache.
|
||||
"""
|
||||
buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
|
||||
buildCache = self.theProject.storage.getCacheFile(nwFiles.BUILD_CACHE)
|
||||
if not isinstance(buildCache, Path):
|
||||
return False
|
||||
|
||||
logger.debug("Saving build cache")
|
||||
try:
|
||||
with open(buildCache, mode="w+", encoding="utf-8") as outFile:
|
||||
@@ -1119,7 +1161,7 @@ class GuiBuildNovel(QDialog):
|
||||
logger.debug("Saving GuiBuildNovel settings")
|
||||
|
||||
# Formatting
|
||||
self.theProject.setTitleFormat({
|
||||
self.theProject.data.setTitleFormat({
|
||||
"title": self.fmtTitle.text().strip(),
|
||||
"chapter": self.fmtChapter.text().strip(),
|
||||
"unnumbered": self.fmtUnnumbered.text().strip(),
|
||||
@@ -1146,6 +1188,7 @@ class GuiBuildNovel(QDialog):
|
||||
incBodyText = self.includeBody.isChecked()
|
||||
replaceTabs = self.replaceTabs.isChecked()
|
||||
replaceUCode = self.replaceUCode.isChecked()
|
||||
rootFilter = self._generateRootFilter()
|
||||
|
||||
mainSplit = self.mainSplit.sizes()
|
||||
boxWidth = self.mainConf.rpxInt(mainSplit[0])
|
||||
@@ -1175,6 +1218,7 @@ class GuiBuildNovel(QDialog):
|
||||
pOptions.setValue("GuiBuildNovel", "incBodyText", incBodyText)
|
||||
pOptions.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
|
||||
pOptions.setValue("GuiBuildNovel", "replaceUCode", replaceUCode)
|
||||
pOptions.setValue("GuiBuildNovel", "rootFilter", rootFilter)
|
||||
pOptions.saveSettings()
|
||||
|
||||
return
|
||||
@@ -1194,7 +1238,7 @@ class GuiBuildNovel(QDialog):
|
||||
class GuiBuildNovelDocView(QTextBrowser):
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QTextBrowser.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiBuildNovelDocView ...")
|
||||
|
||||
@@ -1223,10 +1267,7 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
self.setFont(theFont)
|
||||
|
||||
# Set the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
|
||||
docPalette = self.palette()
|
||||
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
|
||||
@@ -1343,7 +1384,7 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
def resizeEvent(self, theEvent):
|
||||
"""Make sure the document title is the same width as the window.
|
||||
"""
|
||||
QTextBrowser.resizeEvent(self, theEvent)
|
||||
super().resizeEvent(theEvent)
|
||||
self._updateDocMargins()
|
||||
return
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
import novelwriter
|
||||
@@ -34,8 +33,8 @@ from PyQt5.QtWidgets import (
|
||||
QSpinBox
|
||||
)
|
||||
|
||||
from novelwriter.gui.custom import QSwitch
|
||||
from novelwriter.common import readTextFile
|
||||
from novelwriter.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiLipsum(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiLipsum ...")
|
||||
self.setObjectName("GuiLipsum")
|
||||
@@ -120,7 +119,7 @@ class GuiLipsum(QDialog):
|
||||
def _doInsert(self):
|
||||
"""Load the text and insert it in the open document.
|
||||
"""
|
||||
lipsumFile = os.path.join(self.mainConf.assetPath, "text", "lipsum.txt")
|
||||
lipsumFile = self.mainConf.assetPath("text") / "lipsum.txt"
|
||||
lipsumText = readTextFile(lipsumFile).splitlines()
|
||||
|
||||
if self.randSwitch.isChecked():
|
||||
|
||||
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter.common import makeFileNameSafe
|
||||
from novelwriter.gui.custom import QSwitch
|
||||
from novelwriter.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,7 +49,7 @@ PAGE_FINAL = 4
|
||||
class GuiProjectWizard(QWizard):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWizard.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectWizard ...")
|
||||
self.setObjectName("GuiProjectWizard")
|
||||
@@ -88,7 +88,7 @@ class GuiProjectWizard(QWizard):
|
||||
class ProjWizardIntroPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theWizard = theWizard
|
||||
@@ -158,7 +158,7 @@ class ProjWizardIntroPage(QWizardPage):
|
||||
class ProjWizardFolderPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theWizard = theWizard
|
||||
@@ -209,12 +209,12 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
"""Check that the selected path isn't already being used.
|
||||
"""
|
||||
self.errLabel.setText("")
|
||||
if not QWizardPage.isComplete(self):
|
||||
if not super().isComplete():
|
||||
return False
|
||||
|
||||
setPath = os.path.abspath(os.path.expanduser(self.projPath.text()))
|
||||
parPath = os.path.dirname(setPath)
|
||||
logger.verbose("Path is: %s", setPath)
|
||||
logger.debug("Path is: %s", setPath)
|
||||
if parPath and not os.path.isdir(parPath):
|
||||
self.errLabel.setText(self.tr(
|
||||
"Error: A project folder cannot be created using this path."
|
||||
@@ -236,12 +236,9 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
def _doBrowse(self):
|
||||
"""Select a project folder.
|
||||
"""
|
||||
lastPath = self.mainConf.lastPath
|
||||
if not os.path.isdir(lastPath):
|
||||
lastPath = ""
|
||||
|
||||
lastPath = self.mainConf.lastPath()
|
||||
projDir = QFileDialog.getExistingDirectory(
|
||||
self, self.tr("Select Project Folder"), lastPath, options=QFileDialog.ShowDirsOnly
|
||||
self, self.tr("Select Project Folder"), str(lastPath), options=QFileDialog.ShowDirsOnly
|
||||
)
|
||||
if projDir:
|
||||
projName = self.field("projName")
|
||||
@@ -259,7 +256,7 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
class ProjWizardPopulatePage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theWizard = theWizard
|
||||
@@ -315,7 +312,7 @@ class ProjWizardPopulatePage(QWizardPage):
|
||||
class ProjWizardCustomPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theWizard = theWizard
|
||||
@@ -334,13 +331,18 @@ class ProjWizardCustomPage(QWizardPage):
|
||||
|
||||
# Root Folders
|
||||
self.addPlot = QSwitch()
|
||||
self.addChar = QSwitch()
|
||||
self.addWorld = QSwitch()
|
||||
self.addNotes = QSwitch()
|
||||
|
||||
self.addPlot.setChecked(True)
|
||||
self.addPlot.clicked.connect(self._syncSwitches)
|
||||
|
||||
self.addChar = QSwitch()
|
||||
self.addChar.setChecked(True)
|
||||
self.addChar.clicked.connect(self._syncSwitches)
|
||||
|
||||
self.addWorld = QSwitch()
|
||||
self.addWorld.setChecked(False)
|
||||
self.addWorld.clicked.connect(self._syncSwitches)
|
||||
|
||||
self.addNotes = QSwitch()
|
||||
self.addNotes.setChecked(False)
|
||||
|
||||
# Generate Content
|
||||
@@ -391,13 +393,27 @@ class ProjWizardCustomPage(QWizardPage):
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _syncSwitches(self):
|
||||
"""Check if the add notes option should also be switched off.
|
||||
"""
|
||||
addPlot = self.addPlot.isChecked()
|
||||
addChar = self.addChar.isChecked()
|
||||
addWorld = self.addWorld.isChecked()
|
||||
if not (addPlot or addChar or addWorld):
|
||||
self.addNotes.setChecked(False)
|
||||
return
|
||||
|
||||
# END Class ProjWizardCustomPage
|
||||
|
||||
|
||||
class ProjWizardFinalPage(QWizardPage):
|
||||
|
||||
def __init__(self, theWizard):
|
||||
QWizardPage.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theWizard = theWizard
|
||||
@@ -418,7 +434,7 @@ class ProjWizardFinalPage(QWizardPage):
|
||||
def initializePage(self):
|
||||
"""Update the summary information on the final page.
|
||||
"""
|
||||
QWizardPage.initializePage(self)
|
||||
super().initializePage()
|
||||
|
||||
sumList = []
|
||||
sumList.append(self.tr("Project Name: {0}").format(self.field("projName")))
|
||||
|
||||
@@ -23,11 +23,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtGui import QPixmap, QCursor
|
||||
@@ -39,9 +39,9 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.error import formatException
|
||||
from novelwriter.common import formatTime, checkInt, checkIntRange, checkIntTuple
|
||||
from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax
|
||||
from novelwriter.custom import QSwitch
|
||||
from novelwriter.constants import nwConst, nwFiles
|
||||
from novelwriter.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +58,7 @@ class GuiWritingStats(QDialog):
|
||||
FMT_CSV = 1
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiWritingStats ...")
|
||||
self.setObjectName("GuiWritingStats")
|
||||
@@ -112,11 +112,12 @@ class GuiWritingStats(QDialog):
|
||||
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
|
||||
|
||||
hHeader = self.listBox.headerItem()
|
||||
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
if hHeader is not None:
|
||||
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
|
||||
sortCol = checkIntRange(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0)
|
||||
sortCol = minmax(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2)
|
||||
sortOrder = checkIntTuple(
|
||||
pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
|
||||
(Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
|
||||
@@ -361,15 +362,9 @@ class GuiWritingStats(QDialog):
|
||||
return False
|
||||
|
||||
# Generate the file name
|
||||
saveDir = self.mainConf.lastPath
|
||||
if not os.path.isdir(saveDir):
|
||||
saveDir = os.path.expanduser("~")
|
||||
|
||||
fileName = "sessionStats.%s" % fileExt
|
||||
savePath = os.path.join(saveDir, fileName)
|
||||
|
||||
savePath = self.mainConf.lastPath() / f"sessionStats.{fileExt}"
|
||||
savePath, _ = QFileDialog.getSaveFileName(
|
||||
self, self.tr("Save Data As"), savePath, "%s (*.%s)" % (textFmt, fileExt)
|
||||
self, self.tr("Save Data As"), str(savePath), "%s (*.%s)" % (textFmt, fileExt)
|
||||
)
|
||||
if not savePath:
|
||||
return False
|
||||
@@ -438,8 +433,8 @@ class GuiWritingStats(QDialog):
|
||||
ttTime = 0
|
||||
ttIdle = 0
|
||||
|
||||
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
|
||||
if not os.path.isfile(logFile):
|
||||
logFile = self.theProject.storage.getMetaFile(nwFiles.SESS_STATS)
|
||||
if not isinstance(logFile, Path) or not logFile.exists():
|
||||
logger.info("This project has no writing stats logfile")
|
||||
return False
|
||||
|
||||
@@ -449,7 +444,7 @@ class GuiWritingStats(QDialog):
|
||||
if inLine.startswith("#"):
|
||||
if inLine.startswith("# Offset"):
|
||||
self.wordOffset = checkInt(inLine[9:].strip(), 0)
|
||||
logger.verbose(
|
||||
logger.debug(
|
||||
"Initial word count when log was started is %d" % self.wordOffset
|
||||
)
|
||||
continue
|
||||
|
||||