Assemble most of the Manuscript Build tool
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – Init File
|
novelWriter – Init File
|
||||||
=======================
|
=======================
|
||||||
Application initialisation
|
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2018-09-22 [0.0.1]
|
Created: 2018-09-22 [0.0.1]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
|||||||
add = typ_plus.svg
|
add = typ_plus.svg
|
||||||
backward = typ_chevron-left.svg
|
backward = typ_chevron-left.svg
|
||||||
bookmark = typ_bookmark.svg
|
bookmark = typ_bookmark.svg
|
||||||
|
browse = typ_folder-open.svg
|
||||||
build_excluded = typ_cancel.svg
|
build_excluded = typ_cancel.svg
|
||||||
build_filtered = typ_filter.svg
|
build_filtered = typ_filter.svg
|
||||||
build_included = typ_pin.svg
|
build_included = typ_pin.svg
|
||||||
@@ -40,6 +41,7 @@ cls_world = typ_location.svg
|
|||||||
cross = typ_times.svg
|
cross = typ_times.svg
|
||||||
down = typ_chevron-down.svg
|
down = typ_chevron-down.svg
|
||||||
edit = typ_pencil.svg
|
edit = typ_pencil.svg
|
||||||
|
export = typ_export-button.svg
|
||||||
forward = typ_chevron-right.svg
|
forward = typ_chevron-right.svg
|
||||||
maximise = typ_arrow-maximise.svg
|
maximise = typ_arrow-maximise.svg
|
||||||
menu = typ_th-menu.svg
|
menu = typ_th-menu.svg
|
||||||
@@ -57,6 +59,7 @@ proj_title = mixed_document-title.svg
|
|||||||
reference = typ_at.svg
|
reference = typ_at.svg
|
||||||
refresh = typ_refresh.svg
|
refresh = typ_refresh.svg
|
||||||
remove = typ_minus.svg
|
remove = typ_minus.svg
|
||||||
|
revert = typ_refresh-flipped.svg
|
||||||
search = typ_search.svg
|
search = typ_search.svg
|
||||||
search_cancel = typ_cancel-grey.svg
|
search_cancel = typ_cancel-grey.svg
|
||||||
search_case = nw_search-case.svg
|
search_case = nw_search-case.svg
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m6.4444 16.444v0.55556c1.8789-2.8644 4-4.3922 6.6667-4.4444v3.3333c0 0.61222 0.56778 1.1111 1.27 1.1111 0.40444 0 0.75-0.17556 0.98111-0.43444 2.1478-2.2544 6.6378-6.7878 6.6378-6.7878s-4.49-4.5356-6.6389-6.8189c-0.23111-0.22778-0.57556-0.40333-0.98-0.40333-0.70222 0-1.27 0.49667-1.27 1.1111v3.3333c-5.1778 0-6.6667 5.4122-6.6667 9.4444zm-3.3333 5h15.556c0.61444 0 1.1111-0.49778 1.1111-1.1111v-6.7178c-0.73778 0.75111-1.5156 1.5478-2.2222 2.2744v3.3322h-13.333v-13.333h7.7778v-2.2222h-8.8889c-0.61444 0-1.1111 0.49778-1.1111 1.1111v15.556c0 0.61333 0.49667 1.1111 1.1111 1.1111z" fill="#aeaeae" stroke-width="1.1111"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 775 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m21.316 8.1937h-2.2836c-0.38061-1.1418-1.4273-1.903-2.6643-1.903h-5.7091c0-1.0466-0.85636-1.903-1.903-1.903h-3.9012c-1.6175 0-2.8545 1.237-2.8545 2.8545v9.5154c0 1.6175 1.237 2.8545 2.8545 2.8545h11.418c1.6175 0 3.2352-1.237 3.6157-2.8545l2.0934-7.6121c0.09515-0.57091-0.1903-0.95154-0.66605-0.95154zm-17.413 0.95154v-1.903c0-0.57091 0.38061-0.95154 0.95154-0.95154h3.8061c0 1.0466 0.85636 1.903 1.903 1.903h5.7091c0.57091 0 0.95154 0.38061 0.95154 0.95154h-10.562c-0.57091 0-1.0466 0.38061-1.237 0.95154l-1.5225 5.9945zm14.177 7.1363c-0.1903 0.76121-1.0466 1.4273-1.8079 1.4273h-11.418s-0.38061-0.1903-0.1903-0.76121l1.8079-6.6605c0-0.09515 0.1903-0.1903 0.28545-0.1903h13.035z" fill="#aeaeae" stroke-width=".95154"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 873 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10.837 12.256h-6.8373v-6.8347c0.010667-1.5131 0.89467-1.877 1.964-0.80652l1.5387 1.5437c1.3533-1.0598 3.0093-1.6397 4.7547-1.6397 2.0667 0 4.012 0.80518 5.472 2.2676 1.4653 1.4557 2.2707 3.4034 2.2707 5.4696 0 2.0703-0.80533 4.0153-2.268 5.475-1.4627 1.4624-3.4067 2.2689-5.4747 2.2689-2.0667 0-4.012-0.80652-5.4747-2.2702-0.39467-0.39593-0.744-0.82784-1.04-1.2864-0.46267-0.7212-0.25333-1.6797 0.46667-2.1396 0.71867-0.46125 1.6773-0.25195 2.1387 0.46658 0.17733 0.27595 0.38933 0.53323 0.624 0.77319 0.87867 0.87717 2.0453 1.3597 3.2853 1.3597s2.4067-0.48124 3.2853-1.3597c0.876-0.87717 1.36-2.0436 1.36-3.2861 0-1.2398-0.48267-2.4062-1.36-3.2807-0.87867-0.87717-2.0453-1.3611-3.2853-1.3611-0.91733 0-1.7947 0.26795-2.5453 0.76252l1.9307 1.9343c1.0707 1.0691 0.70667 1.9436-0.80533 1.9436z" fill="#9c9" stroke-width="1.3332"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 984 B |
@@ -19,6 +19,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
|||||||
add = typ_plus.svg
|
add = typ_plus.svg
|
||||||
backward = typ_chevron-left.svg
|
backward = typ_chevron-left.svg
|
||||||
bookmark = typ_bookmark.svg
|
bookmark = typ_bookmark.svg
|
||||||
|
browse = typ_folder-open.svg
|
||||||
build_excluded = typ_cancel.svg
|
build_excluded = typ_cancel.svg
|
||||||
build_filtered = typ_filter.svg
|
build_filtered = typ_filter.svg
|
||||||
build_included = typ_pin.svg
|
build_included = typ_pin.svg
|
||||||
@@ -40,6 +41,7 @@ cls_world = typ_location.svg
|
|||||||
cross = typ_times.svg
|
cross = typ_times.svg
|
||||||
down = typ_chevron-down.svg
|
down = typ_chevron-down.svg
|
||||||
edit = typ_pencil.svg
|
edit = typ_pencil.svg
|
||||||
|
export = typ_export-button.svg
|
||||||
forward = typ_chevron-right.svg
|
forward = typ_chevron-right.svg
|
||||||
maximise = typ_arrow-maximise.svg
|
maximise = typ_arrow-maximise.svg
|
||||||
menu = typ_th-menu.svg
|
menu = typ_th-menu.svg
|
||||||
@@ -57,6 +59,7 @@ proj_title = mixed_document-title.svg
|
|||||||
reference = typ_at.svg
|
reference = typ_at.svg
|
||||||
refresh = typ_refresh.svg
|
refresh = typ_refresh.svg
|
||||||
remove = typ_minus.svg
|
remove = typ_minus.svg
|
||||||
|
revert = typ_refresh-flipped.svg
|
||||||
search = typ_search.svg
|
search = typ_search.svg
|
||||||
search_cancel = typ_cancel-grey.svg
|
search_cancel = typ_cancel-grey.svg
|
||||||
search_case = nw_search-case.svg
|
search_case = nw_search-case.svg
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m6.4444 16.444v0.55556c1.8789-2.8644 4-4.3922 6.6667-4.4444v3.3333c0 0.61222 0.56778 1.1111 1.27 1.1111 0.40444 0 0.75-0.17556 0.98111-0.43444 2.1478-2.2544 6.6378-6.7878 6.6378-6.7878s-4.49-4.5356-6.6389-6.8189c-0.23111-0.22778-0.57556-0.40333-0.98-0.40333-0.70222 0-1.27 0.49667-1.27 1.1111v3.3333c-5.1778 0-6.6667 5.4122-6.6667 9.4444zm-3.3333 5h15.556c0.61444 0 1.1111-0.49778 1.1111-1.1111v-6.7178c-0.73778 0.75111-1.5156 1.5478-2.2222 2.2744v3.3322h-13.333v-13.333h7.7778v-2.2222h-8.8889c-0.61444 0-1.1111 0.49778-1.1111 1.1111v15.556c0 0.61333 0.49667 1.1111 1.1111 1.1111z" fill-opacity=".72157" stroke-width="1.1111"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 782 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m21.316 8.1937h-2.2836c-0.38061-1.1418-1.4273-1.903-2.6643-1.903h-5.7091c0-1.0466-0.85636-1.903-1.903-1.903h-3.9012c-1.6175 0-2.8545 1.237-2.8545 2.8545v9.5154c0 1.6175 1.237 2.8545 2.8545 2.8545h11.418c1.6175 0 3.2352-1.237 3.6157-2.8545l2.0934-7.6121c0.09515-0.57091-0.1903-0.95154-0.66605-0.95154zm-17.413 0.95154v-1.903c0-0.57091 0.38061-0.95154 0.95154-0.95154h3.8061c0 1.0466 0.85636 1.903 1.903 1.903h5.7091c0.57091 0 0.95154 0.38061 0.95154 0.95154h-10.562c-0.57091 0-1.0466 0.38061-1.237 0.95154l-1.5225 5.9945zm14.177 7.1363c-0.1903 0.76121-1.0466 1.4273-1.8079 1.4273h-11.418s-0.38061-0.1903-0.1903-0.76121l1.8079-6.6605c0-0.09515 0.1903-0.1903 0.28545-0.1903h13.035z" fill-opacity=".72157" stroke-width="1.0467"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 880 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10.837 12.256h-6.8373v-6.8347c0.010667-1.5131 0.89467-1.877 1.964-0.80652l1.5387 1.5437c1.3533-1.0598 3.0093-1.6397 4.7547-1.6397 2.0667 0 4.012 0.80518 5.472 2.2676 1.4653 1.4557 2.2707 3.4034 2.2707 5.4696 0 2.0703-0.80533 4.0153-2.268 5.475-1.4627 1.4624-3.4067 2.2689-5.4747 2.2689-2.0667 0-4.012-0.80652-5.4747-2.2702-0.39467-0.39593-0.744-0.82784-1.04-1.2864-0.46267-0.7212-0.25333-1.6797 0.46667-2.1396 0.71867-0.46125 1.6773-0.25195 2.1387 0.46658 0.17733 0.27595 0.38933 0.53323 0.624 0.77319 0.87867 0.87717 2.0453 1.3597 3.2853 1.3597s2.4067-0.48124 3.2853-1.3597c0.876-0.87717 1.36-2.0436 1.36-3.2861 0-1.2398-0.48267-2.4062-1.36-3.2807-0.87867-0.87717-2.0453-1.3611-3.2853-1.3611-0.91733 0-1.7947 0.26795-2.5453 0.76252l1.9307 1.9343c1.0707 1.0691 0.70667 1.9436-0.80533 1.9436z" fill="#718c00" stroke-width="1.3332"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 987 B |
@@ -27,6 +27,7 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import unicodedata
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@@ -469,8 +470,7 @@ def xmlIndent(tree: ET.Element | ET.ElementTree):
|
|||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|
||||||
def readTextFile(path: str | Path) -> str:
|
def readTextFile(path: str | Path) -> str:
|
||||||
"""Read the content of a text file in a robust manner.
|
"""Read the content of a text file in a robust manner."""
|
||||||
"""
|
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return ""
|
return ""
|
||||||
@@ -482,14 +482,13 @@ def readTextFile(path: str | Path) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def makeFileNameSafe(value: str) -> str:
|
def makeFileNameSafe(text: str) -> str:
|
||||||
"""Returns a filename safe string of the value.
|
"""Return a filename safe string.
|
||||||
|
See: https://unicode.org/reports/tr15/#Norm_Forms
|
||||||
"""
|
"""
|
||||||
clean = ""
|
text = unicodedata.normalize("NFKC", text).strip()
|
||||||
for c in str(value).strip():
|
allowed = (" ", ".", "-", "_")
|
||||||
if c.isalpha() or c.isdigit() or c == " ":
|
return "".join(c for c in text if c.isalnum() or c in allowed)
|
||||||
clean += c
|
|
||||||
return clean
|
|
||||||
|
|
||||||
|
|
||||||
def sha256sum(path: str | Path) -> str | None:
|
def sha256sum(path: str | Path) -> str | None:
|
||||||
|
|||||||
@@ -199,6 +199,16 @@ class nwLabels:
|
|||||||
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||||||
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
|
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
|
||||||
}
|
}
|
||||||
|
BUILD_FORMATS = {
|
||||||
|
"odt": ("odt", QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)")),
|
||||||
|
"fodt": ("fodt", QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)")),
|
||||||
|
"html": ("html", QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)")),
|
||||||
|
"nwd": ("nwd", QT_TRANSLATE_NOOP("Constant", "novelWriter Markdown (.nwd)")),
|
||||||
|
"md": ("md", QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)")),
|
||||||
|
"md+": ("md", QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)")),
|
||||||
|
"jhtml": ("json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)")),
|
||||||
|
"jnwd": ("json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markdown (.json)")),
|
||||||
|
}
|
||||||
|
|
||||||
# END Class nwLabels
|
# END Class nwLabels
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from pathlib import Path
|
|||||||
from PyQt5.QtCore import QT_TRANSLATE_NOOP
|
from PyQt5.QtCore import QT_TRANSLATE_NOOP
|
||||||
|
|
||||||
from novelwriter.common import checkUuid, isHandle, jsonEncode
|
from novelwriter.common import checkUuid, isHandle, jsonEncode
|
||||||
from novelwriter.constants import nwFiles, nwHeadFmt
|
from novelwriter.constants import nwFiles, nwHeadFmt, nwLabels
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
@@ -72,6 +72,9 @@ SETTINGS_TEMPLATE = {
|
|||||||
"format.replaceTabs": (bool, False),
|
"format.replaceTabs": (bool, False),
|
||||||
"odt.addColours": (bool, True),
|
"odt.addColours": (bool, True),
|
||||||
"html.addStyles": (bool, False),
|
"html.addStyles": (bool, False),
|
||||||
|
"build.splitNovel": (bool, False),
|
||||||
|
"build.splitNotes": (bool, False),
|
||||||
|
"build.splitChapters": (bool, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
SETTINGS_LABELS = {
|
SETTINGS_LABELS = {
|
||||||
@@ -112,6 +115,11 @@ SETTINGS_LABELS = {
|
|||||||
|
|
||||||
"html": QT_TRANSLATE_NOOP("Builds", "HTML"),
|
"html": QT_TRANSLATE_NOOP("Builds", "HTML"),
|
||||||
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
|
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
|
||||||
|
|
||||||
|
"build": QT_TRANSLATE_NOOP("Builds", "Build Options"),
|
||||||
|
"build.splitNovel": QT_TRANSLATE_NOOP("Builds", "Split Novel Root Folders"),
|
||||||
|
"build.splitNotes": QT_TRANSLATE_NOOP("Builds", "Split Note Root Folders"),
|
||||||
|
"build.splitChapters": QT_TRANSLATE_NOOP("Builds", "Split By Chapter Document"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +145,9 @@ class BuildSettings:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._uuid = str(uuid.uuid4())
|
self._uuid = str(uuid.uuid4())
|
||||||
|
self._path = Path.home()
|
||||||
|
self._build = ""
|
||||||
|
self._format = "odt"
|
||||||
self._skipRoot = set()
|
self._skipRoot = set()
|
||||||
self._excluded = set()
|
self._excluded = set()
|
||||||
self._included = set()
|
self._included = set()
|
||||||
@@ -158,6 +169,23 @@ class BuildSettings:
|
|||||||
"""The build ID as an UUID."""
|
"""The build ID as an UUID."""
|
||||||
return self._uuid
|
return self._uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lastPath(self) -> Path:
|
||||||
|
"""The last used build path."""
|
||||||
|
if self._path.is_dir():
|
||||||
|
return self._path
|
||||||
|
return Path.home()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lastBuildName(self) -> str:
|
||||||
|
"""The last used build name."""
|
||||||
|
return self._build
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lastFormat(self) -> str:
|
||||||
|
"""The last used build format."""
|
||||||
|
return self._format
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed(self) -> bool:
|
def changed(self) -> bool:
|
||||||
"""The changed status of the build."""
|
"""The changed status of the build."""
|
||||||
@@ -214,6 +242,30 @@ class BuildSettings:
|
|||||||
self._uuid = value
|
self._uuid = value
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def setLastPath(self, path: Path | str | None):
|
||||||
|
"""Set the last used build path."""
|
||||||
|
if isinstance(path, str):
|
||||||
|
path = Path(path)
|
||||||
|
if isinstance(path, Path) and path.is_dir():
|
||||||
|
self._path = path
|
||||||
|
else:
|
||||||
|
self._path = Path.home()
|
||||||
|
self._changed = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def setLastBuildName(self, name: str):
|
||||||
|
"""Set the last used build name."""
|
||||||
|
self._build = str(name).strip()
|
||||||
|
self._changed = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def setLastFormat(self, key: str):
|
||||||
|
"""Set the last used build format."""
|
||||||
|
if key in nwLabels.BUILD_FORMATS:
|
||||||
|
self._format = key
|
||||||
|
self._changed = True
|
||||||
|
return
|
||||||
|
|
||||||
def setFiltered(self, tHandle: str):
|
def setFiltered(self, tHandle: str):
|
||||||
"""Set an item as filtered."""
|
"""Set an item as filtered."""
|
||||||
self._excluded.discard(tHandle)
|
self._excluded.discard(tHandle)
|
||||||
@@ -325,6 +377,9 @@ class BuildSettings:
|
|||||||
return {
|
return {
|
||||||
"name": self._name,
|
"name": self._name,
|
||||||
"uuid": self._uuid,
|
"uuid": self._uuid,
|
||||||
|
"path": str(self._path),
|
||||||
|
"build": self._build,
|
||||||
|
"format": self._format,
|
||||||
"settings": self._settings.copy(),
|
"settings": self._settings.copy(),
|
||||||
"content": {
|
"content": {
|
||||||
"included": list(self._included),
|
"included": list(self._included),
|
||||||
@@ -343,6 +398,9 @@ class BuildSettings:
|
|||||||
|
|
||||||
self.setName(data.get("name", ""))
|
self.setName(data.get("name", ""))
|
||||||
self.setBuildID(data.get("uuid", ""))
|
self.setBuildID(data.get("uuid", ""))
|
||||||
|
self.setLastPath(data.get("path", None))
|
||||||
|
self.setLastBuildName(data.get("build", ""))
|
||||||
|
self.setLastFormat(data.get("format", "odt"))
|
||||||
if isinstance(included, list):
|
if isinstance(included, list):
|
||||||
self._included = set([h for h in included if isHandle(h)])
|
self._included = set([h for h in included if isHandle(h)])
|
||||||
if isinstance(excluded, list):
|
if isinstance(excluded, list):
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ VALID_MAP = {
|
|||||||
"GuiManuscript": {
|
"GuiManuscript": {
|
||||||
"winWidth", "winHeight", "optsWidth", "viewWidth"
|
"winWidth", "winHeight", "optsWidth", "viewWidth"
|
||||||
},
|
},
|
||||||
|
"GuiManuscriptBuild": {
|
||||||
|
"winWidth", "winHeight", "fmtWidth", "optsWidth"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – Custom Widget: Switch Box
|
novelWriter – Custom Widget: Switch Box
|
||||||
=======================================
|
=======================================
|
||||||
A box of icons, labels and switches
|
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2023-04-16 [2.1b1]
|
Created: 2023-04-16 [2.1b1]
|
||||||
@@ -22,32 +21,35 @@ General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal
|
from PyQt5.QtCore import Qt, pyqtSignal
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QSizePolicy, QWidget
|
from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QSizePolicy, QWidget
|
||||||
|
|
||||||
from novelwriter.extensions.switch import NSwitch
|
from novelwriter.extensions.switch import NSwitch
|
||||||
|
|
||||||
|
|
||||||
class NSwitchBox(QScrollArea):
|
class NSwitchBox(QScrollArea):
|
||||||
|
"""Extension: Switch Box Widget
|
||||||
|
|
||||||
|
A widget that can hold a list of switches with labels and optional
|
||||||
|
icons. The switch toggles emits a common signal with a switch key.
|
||||||
|
"""
|
||||||
|
|
||||||
switchToggled = pyqtSignal(str, bool)
|
switchToggled = pyqtSignal(str, bool)
|
||||||
|
|
||||||
def __init__(self, parent, baseSize):
|
def __init__(self, parent: QWidget, baseSize: int):
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self._index = 0
|
self._index = 0
|
||||||
self._hSwitch = baseSize
|
self._hSwitch = baseSize
|
||||||
self._wSwitch = 2*self._hSwitch
|
self._wSwitch = 2*self._hSwitch
|
||||||
self._sIcon = baseSize
|
self._sIcon = baseSize
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Rebuild the content of the core widget.
|
"""Rebuild the content of the core widget."""
|
||||||
"""
|
|
||||||
self._content = QGridLayout()
|
self._content = QGridLayout()
|
||||||
self._content.setColumnStretch(1, 1)
|
self._content.setColumnStretch(1, 1)
|
||||||
|
|
||||||
@@ -60,9 +62,8 @@ class NSwitchBox(QScrollArea):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def addLabel(self, text):
|
def addLabel(self, text: str):
|
||||||
"""Add a header label to the content box.
|
"""Add a header label to the content box."""
|
||||||
"""
|
|
||||||
label = QLabel(text)
|
label = QLabel(text)
|
||||||
font = label.font()
|
font = label.font()
|
||||||
font.setBold(True)
|
font.setBold(True)
|
||||||
@@ -71,9 +72,8 @@ class NSwitchBox(QScrollArea):
|
|||||||
self._bumpIndex()
|
self._bumpIndex()
|
||||||
return
|
return
|
||||||
|
|
||||||
def addItem(self, qIcon, text, identifier, default=False):
|
def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False):
|
||||||
"""Add an item to the content box.
|
"""Add an item to the content box."""
|
||||||
"""
|
|
||||||
icon = QLabel("")
|
icon = QLabel("")
|
||||||
icon.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
icon.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||||
icon.setPixmap(qIcon.pixmap(self._sIcon, self._sIcon))
|
icon.setPixmap(qIcon.pixmap(self._sIcon, self._sIcon))
|
||||||
@@ -100,13 +100,17 @@ class NSwitchBox(QScrollArea):
|
|||||||
self._bumpIndex()
|
self._bumpIndex()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def setInnerContentsMargins(self, left: int, top: int, right: int, bottom: int):
|
||||||
|
"""Set the contents margins of the inner layout."""
|
||||||
|
self._content.setContentsMargins(left, top, right, bottom)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _emitSwitchSignal(self, identifier, state):
|
def _emitSwitchSignal(self, identifier: str, state: bool):
|
||||||
"""Emit a signal for a switch toggle.
|
"""Emit a signal for a switch toggle."""
|
||||||
"""
|
|
||||||
self.switchToggled.emit(identifier, state)
|
self.switchToggled.emit(identifier, state)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -454,9 +454,9 @@ class GuiIcons:
|
|||||||
"view_novel", "view_outline",
|
"view_novel", "view_outline",
|
||||||
|
|
||||||
# General Button Icons
|
# General Button Icons
|
||||||
"add", "backward", "bookmark", "checked", "close", "cross", "down", "edit", "forward",
|
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
|
||||||
"maximise", "menu", "minimise", "noncheckable", "reference", "refresh", "remove",
|
"export", "forward", "maximise", "menu", "minimise", "noncheckable", "reference",
|
||||||
"search_replace", "search", "settings", "unchecked", "up",
|
"refresh", "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
|
||||||
|
|
||||||
# Switches
|
# Switches
|
||||||
"sticky-on", "sticky-off",
|
"sticky-on", "sticky-off",
|
||||||
|
|||||||
@@ -25,7 +25,23 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QDialog, QWidget
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, pyqtSlot
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAbstractButton, QDialog, QDialogButtonBox, QFrame, QGridLayout,
|
||||||
|
QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QProgressBar,
|
||||||
|
QPushButton, QSplitter, QVBoxLayout, QWidget
|
||||||
|
)
|
||||||
|
|
||||||
|
from novelwriter import CONFIG
|
||||||
|
from novelwriter.common import makeFileNameSafe
|
||||||
|
from novelwriter.constants import nwLabels
|
||||||
|
from novelwriter.core.buildsettings import BuildSettings
|
||||||
|
from novelwriter.extensions.switchbox import NSwitchBox
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from novelwriter.guimain import GuiMain
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -37,13 +53,146 @@ class GuiManuscriptBuild(QDialog):
|
|||||||
independently of the Manuscript Build Tool.
|
independently of the Manuscript Build Tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent: QWidget):
|
def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings):
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
logger.debug("Create: GuiManuscriptBuild")
|
logger.debug("Create: GuiManuscriptBuild")
|
||||||
self.setObjectName("GuiManuscriptBuild")
|
self.setObjectName("GuiManuscriptBuild")
|
||||||
|
|
||||||
|
self.mainGui = mainGui
|
||||||
|
self.mainTheme = mainGui.mainTheme
|
||||||
|
self.theProject = mainGui.theProject
|
||||||
|
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
|
self._build = build
|
||||||
|
|
||||||
|
self.setWindowTitle(self.tr("Build Manuscript"))
|
||||||
|
self.setMinimumWidth(CONFIG.pxInt(500))
|
||||||
|
self.setMinimumHeight(CONFIG.pxInt(250))
|
||||||
|
|
||||||
|
wWin = CONFIG.pxInt(660)
|
||||||
|
hWin = CONFIG.pxInt(350)
|
||||||
|
|
||||||
|
pOptions = self.theProject.options
|
||||||
|
self.resize(
|
||||||
|
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winWidth", wWin)),
|
||||||
|
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winHeight", hWin))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Formats
|
||||||
|
# =======
|
||||||
|
|
||||||
|
self.lblFormat = QLabel("<b>{0}</b>".format(self.tr("Build Format")))
|
||||||
|
self.listFormats = QListWidget()
|
||||||
|
current = None
|
||||||
|
for key, (_, label) in nwLabels.BUILD_FORMATS.items():
|
||||||
|
item = QListWidgetItem()
|
||||||
|
item.setText(label)
|
||||||
|
item.setData(Qt.UserRole, key)
|
||||||
|
self.listFormats.addItem(item)
|
||||||
|
if key == self._build.lastFormat:
|
||||||
|
current = item
|
||||||
|
if current:
|
||||||
|
self.listFormats.setCurrentItem(current)
|
||||||
|
|
||||||
|
self.formatBox = QVBoxLayout()
|
||||||
|
self.formatBox.addWidget(self.lblFormat, 0)
|
||||||
|
self.formatBox.addWidget(self.listFormats, 1)
|
||||||
|
self.formatBox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.formatWidget = QWidget()
|
||||||
|
self.formatWidget.setLayout(self.formatBox)
|
||||||
|
self.formatWidget.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Build Controls
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
# Build Options
|
||||||
|
self.swtOptions = NSwitchBox(self, self.mainTheme.baseIconSize)
|
||||||
|
self.swtOptions.switchToggled.connect(self._applyBuildOptions)
|
||||||
|
self.swtOptions.setFrameStyle(QFrame.NoFrame)
|
||||||
|
self.swtOptions.setInnerContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.swtOptions.addLabel(self._build.getLabel("build"))
|
||||||
|
self.swtOptions.addItem(
|
||||||
|
self.mainTheme.getIcon("cls_novel"), self._build.getLabel("build.splitNovel"),
|
||||||
|
"build.splitNovel", default=self._build.getBool("build.splitNovel")
|
||||||
|
)
|
||||||
|
self.swtOptions.addItem(
|
||||||
|
self.mainTheme.getIcon("cls_custom"), self._build.getLabel("build.splitNotes"),
|
||||||
|
"build.splitNotes", default=self._build.getBool("build.splitNotes")
|
||||||
|
)
|
||||||
|
self.swtOptions.addItem(
|
||||||
|
self.mainTheme.getIcon("proj_chapter"), self._build.getLabel("build.splitChapters"),
|
||||||
|
"build.splitChapters", default=self._build.getBool("build.splitChapters")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dialog Controls
|
||||||
|
# ===============
|
||||||
|
|
||||||
|
# Build Path
|
||||||
|
self.lblPath = QLabel(self.tr("Build Folder"))
|
||||||
|
self.buildPath = QLineEdit()
|
||||||
|
self.buildPath.setText(str(self._build.lastPath))
|
||||||
|
self.btnBrowse = QPushButton(self.mainTheme.getIcon("browse"), "")
|
||||||
|
|
||||||
|
self.pathBox = QHBoxLayout()
|
||||||
|
self.pathBox.addWidget(self.buildPath)
|
||||||
|
self.pathBox.addWidget(self.btnBrowse)
|
||||||
|
|
||||||
|
# Build Name
|
||||||
|
self.lblName = QLabel(self.tr("Build Name"))
|
||||||
|
self.buildName = QLineEdit()
|
||||||
|
self.btnReset = QPushButton(self.mainTheme.getIcon("revert"), "")
|
||||||
|
self.btnReset.setToolTip(self.tr("Reset Build Name to default"))
|
||||||
|
self.btnReset.clicked.connect(self._doResetBuildName)
|
||||||
|
|
||||||
|
self.nameBox = QHBoxLayout()
|
||||||
|
self.nameBox.addWidget(self.buildName)
|
||||||
|
self.nameBox.addWidget(self.btnReset)
|
||||||
|
|
||||||
|
# Build Progress
|
||||||
|
self.lblProgress = QLabel(self.tr("Build Progress"))
|
||||||
|
self.buildProgress = QProgressBar()
|
||||||
|
|
||||||
|
# Dialog Buttons
|
||||||
|
self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
|
||||||
|
self.dlgButtons.addButton(
|
||||||
|
QPushButton(self.mainTheme.getIcon("export"), self.tr("&Build")),
|
||||||
|
QDialogButtonBox.ActionRole
|
||||||
|
)
|
||||||
|
self.dlgButtons.clicked.connect(self._dialogButtonClicked)
|
||||||
|
|
||||||
|
# Assemble GUI
|
||||||
|
# ============
|
||||||
|
|
||||||
|
self.mainSplit = QSplitter()
|
||||||
|
self.mainSplit.addWidget(self.formatWidget)
|
||||||
|
self.mainSplit.addWidget(self.swtOptions)
|
||||||
|
self.mainSplit.setHandleWidth(CONFIG.pxInt(16))
|
||||||
|
self.mainSplit.setCollapsible(0, False)
|
||||||
|
self.mainSplit.setCollapsible(1, False)
|
||||||
|
self.mainSplit.setSizes([
|
||||||
|
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "fmtWidth", int(0.45*wWin))),
|
||||||
|
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "optsWidth", int(0.55*wWin))),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.outerBox = QGridLayout()
|
||||||
|
self.outerBox.addWidget(self.mainSplit, 0, 0, 1, 2)
|
||||||
|
self.outerBox.addWidget(self.lblPath, 1, 0, 1, 1)
|
||||||
|
self.outerBox.addLayout(self.pathBox, 1, 1, 1, 1)
|
||||||
|
self.outerBox.addWidget(self.lblName, 2, 0, 1, 1)
|
||||||
|
self.outerBox.addLayout(self.nameBox, 2, 1, 1, 1)
|
||||||
|
self.outerBox.addWidget(self.lblProgress, 3, 0, 1, 1)
|
||||||
|
self.outerBox.addWidget(self.buildProgress, 3, 1, 1, 1)
|
||||||
|
self.outerBox.addWidget(self.dlgButtons, 4, 0, 1, 2)
|
||||||
|
|
||||||
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
|
if self._build.lastBuildName:
|
||||||
|
self.buildName.setText(makeFileNameSafe(self._build.lastBuildName))
|
||||||
|
else:
|
||||||
|
self._doResetBuildName()
|
||||||
|
|
||||||
logger.debug("Ready: GuiManuscriptBuild")
|
logger.debug("Ready: GuiManuscriptBuild")
|
||||||
|
|
||||||
@@ -54,4 +203,91 @@ class GuiManuscriptBuild(QDialog):
|
|||||||
logger.debug("Delete: GuiManuscriptBuild")
|
logger.debug("Delete: GuiManuscriptBuild")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Events
|
||||||
|
##
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Capture the user closing the window so we can save GUI
|
||||||
|
settings.
|
||||||
|
"""
|
||||||
|
self._saveSettings()
|
||||||
|
event.accept()
|
||||||
|
self.deleteLater()
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Private Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot(str, bool)
|
||||||
|
def _applyBuildOptions(self, key: str, state: bool):
|
||||||
|
"""Set the build options for the build."""
|
||||||
|
self._build.setValue(key, state)
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot("QAbstractButton*")
|
||||||
|
def _dialogButtonClicked(self, button: QAbstractButton):
|
||||||
|
"""Handle button clicks from the dialog button box."""
|
||||||
|
role = self.dlgButtons.buttonRole(button)
|
||||||
|
if role == QDialogButtonBox.ActionRole:
|
||||||
|
self._runBuild()
|
||||||
|
elif role == QDialogButtonBox.RejectRole:
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _doResetBuildName(self):
|
||||||
|
"""Generate a default build name."""
|
||||||
|
bName = makeFileNameSafe(f"{self.theProject.data.name} - {self._build.name}")
|
||||||
|
self.buildName.setText(bName)
|
||||||
|
self._build.setLastBuildName(bName)
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _runBuild(self) -> bool:
|
||||||
|
"""Run the currently selected build."""
|
||||||
|
bFormat = self._getSelectedFormat()
|
||||||
|
if not bFormat:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bPath = self.buildPath.text()
|
||||||
|
bName = self.buildName.text()
|
||||||
|
|
||||||
|
self._build.setLastFormat(bFormat)
|
||||||
|
self._build.setLastPath(bPath)
|
||||||
|
self._build.setLastBuildName(bName)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _getSelectedFormat(self) -> str | None:
|
||||||
|
"""Get the currently selected format."""
|
||||||
|
items = self.listFormats.selectedItems()
|
||||||
|
if items and isinstance(items[0], QListWidgetItem):
|
||||||
|
return str(items[0].data(Qt.UserRole))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _saveSettings(self):
|
||||||
|
"""Save the user GUI settings."""
|
||||||
|
logger.debug("Saving GuiManuscriptBuild settings")
|
||||||
|
|
||||||
|
winWidth = CONFIG.rpxInt(self.width())
|
||||||
|
winHeight = CONFIG.rpxInt(self.height())
|
||||||
|
|
||||||
|
mainSplit = self.mainSplit.sizes()
|
||||||
|
fmtWidth = CONFIG.rpxInt(mainSplit[0])
|
||||||
|
optsWidth = CONFIG.rpxInt(mainSplit[1])
|
||||||
|
|
||||||
|
pOptions = self.theProject.options
|
||||||
|
pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth)
|
||||||
|
pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight)
|
||||||
|
pOptions.setValue("GuiManuscriptBuild", "fmtWidth", fmtWidth)
|
||||||
|
pOptions.setValue("GuiManuscriptBuild", "optsWidth", optsWidth)
|
||||||
|
pOptions.saveSettings()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
# END Class GuiManuscriptBuild
|
# END Class GuiManuscriptBuild
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from novelwriter.common import checkInt, fuzzyTime
|
|||||||
from novelwriter.core.tohtml import ToHtml
|
from novelwriter.core.tohtml import ToHtml
|
||||||
from novelwriter.core.docbuild import NWBuildDocument
|
from novelwriter.core.docbuild import NWBuildDocument
|
||||||
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
|
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
|
||||||
|
from novelwriter.tools.manusbuild import GuiManuscriptBuild
|
||||||
from novelwriter.tools.manussettings import GuiBuildSettings
|
from novelwriter.tools.manussettings import GuiBuildSettings
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -107,26 +108,16 @@ class GuiManuscript(QDialog):
|
|||||||
self.btnPreview.clicked.connect(self._generatePreview)
|
self.btnPreview.clicked.connect(self._generatePreview)
|
||||||
self.manPreview = _PreviewWidget(self.mainGui)
|
self.manPreview = _PreviewWidget(self.mainGui)
|
||||||
|
|
||||||
|
self.btnBuild = QPushButton(self.tr("Build"))
|
||||||
|
self.btnBuild.clicked.connect(self._buildManuscript)
|
||||||
|
|
||||||
self.menuPrint = QMenu(self)
|
self.menuPrint = QMenu(self)
|
||||||
self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview"))
|
self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview"))
|
||||||
self.aPrintFile = self.menuPrint.addAction(self.tr("Print to PDF"))
|
self.aPrintFile = self.menuPrint.addAction(self.tr("Print to PDF"))
|
||||||
|
|
||||||
self.menuSave = QMenu(self)
|
|
||||||
self.aSaveODT = self.menuSave.addAction(self.tr("Open Document (.odt)"))
|
|
||||||
self.aSaveFODT = self.menuSave.addAction(self.tr("Flat Open Document (.fodt)"))
|
|
||||||
self.aSaveHTM = self.menuSave.addAction(self.tr("novelWriter HTML (.htm)"))
|
|
||||||
self.aSaveNWD = self.menuSave.addAction(self.tr("novelWriter Markdown (.nwd)"))
|
|
||||||
self.aSaveMD = self.menuSave.addAction(self.tr("Standard Markdown (.md)"))
|
|
||||||
self.aSaveGH = self.menuSave.addAction(self.tr("GitHub Markdown (.md)"))
|
|
||||||
self.aSaveJsonH = self.menuSave.addAction(self.tr("JSON + novelWriter HTML (.json)"))
|
|
||||||
self.aSaveJsonM = self.menuSave.addAction(self.tr("JSON + novelWriter Markdown (.json)"))
|
|
||||||
|
|
||||||
self.btnPrint = QPushButton(self.tr("Print"))
|
self.btnPrint = QPushButton(self.tr("Print"))
|
||||||
self.btnPrint.setMenu(self.menuPrint)
|
self.btnPrint.setMenu(self.menuPrint)
|
||||||
|
|
||||||
self.btnSave = QPushButton(self.tr("Save As"))
|
|
||||||
self.btnSave.setMenu(self.menuSave)
|
|
||||||
|
|
||||||
self.btnClose = QPushButton(self.tr("Close"))
|
self.btnClose = QPushButton(self.tr("Close"))
|
||||||
self.btnClose.clicked.connect(self._doClose)
|
self.btnClose.clicked.connect(self._doClose)
|
||||||
|
|
||||||
@@ -139,7 +130,7 @@ class GuiManuscript(QDialog):
|
|||||||
self.buildBox.addWidget(self.btnDelete)
|
self.buildBox.addWidget(self.btnDelete)
|
||||||
|
|
||||||
self.processBox = QHBoxLayout()
|
self.processBox = QHBoxLayout()
|
||||||
self.processBox.addWidget(self.btnSave)
|
self.processBox.addWidget(self.btnBuild)
|
||||||
self.processBox.addWidget(self.btnPrint)
|
self.processBox.addWidget(self.btnPrint)
|
||||||
self.processBox.addWidget(self.btnClose)
|
self.processBox.addWidget(self.btnClose)
|
||||||
|
|
||||||
@@ -283,6 +274,23 @@ class GuiManuscript(QDialog):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _buildManuscript(self):
|
||||||
|
"""Open the build dialog and build the manuscript."""
|
||||||
|
build = self._getSelectedBuild()
|
||||||
|
if build is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
dlgBuild = GuiManuscriptBuild(self, self.mainGui, build)
|
||||||
|
dlgBuild.exec_()
|
||||||
|
|
||||||
|
# After the build is done, save build settings changes
|
||||||
|
if build.changed:
|
||||||
|
self._builds.setBuild(build)
|
||||||
|
self._builds.saveCollection()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _doClose(self):
|
def _doClose(self):
|
||||||
"""Forward the close button to the default close method."""
|
"""Forward the close button to the default close method."""
|
||||||
@@ -514,7 +522,7 @@ class _PreviewWidget(QTextBrowser):
|
|||||||
if self._docTime > 0:
|
if self._docTime > 0:
|
||||||
strBuildTime = "%s (%s)" % (
|
strBuildTime = "%s (%s)" % (
|
||||||
datetime.fromtimestamp(self._docTime).strftime("%x %X"),
|
datetime.fromtimestamp(self._docTime).strftime("%x %X"),
|
||||||
fuzzyTime(time() - self._docTime)
|
fuzzyTime(int(time()) - self._docTime)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
strBuildTime = self.tr("Unknown")
|
strBuildTime = self.tr("Unknown")
|
||||||
|
|||||||
@@ -25,24 +25,23 @@ import hashlib
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mocked import causeOSError
|
|
||||||
from tools import writeFile
|
from tools import writeFile
|
||||||
|
from mocked import causeOSError
|
||||||
|
|
||||||
from novelwriter.guimain import GuiMain
|
from novelwriter.guimain import GuiMain
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkStringNone, checkString, checkInt, checkFloat, checkBool, checkHandle,
|
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
|
||||||
checkUuid, checkPath, isHandle, isTitleTag, isItemClass, isItemType,
|
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
||||||
isItemLayout, hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp,
|
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
|
||||||
formatTime, simplified, yesNo, transferCase, fuzzyTime, numberToRoman,
|
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
||||||
jsonEncode, readTextFile, makeFileNameSafe, sha256sum, getGuiItem,
|
numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified,
|
||||||
NWConfigParser
|
transferCase, yesNo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_CheckStringNone():
|
def testBaseCommon_CheckStringNone():
|
||||||
"""Test the checkStringNone function.
|
"""Test the checkStringNone function."""
|
||||||
"""
|
|
||||||
assert checkStringNone("Stuff", "NotNone") == "Stuff"
|
assert checkStringNone("Stuff", "NotNone") == "Stuff"
|
||||||
assert checkStringNone("None", "NotNone") is None
|
assert checkStringNone("None", "NotNone") is None
|
||||||
assert checkStringNone(None, "NotNone") is None
|
assert checkStringNone(None, "NotNone") is None
|
||||||
@@ -151,8 +150,7 @@ def testBaseCommon_CheckBool():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_CheckHandle():
|
def testBaseCommon_CheckHandle():
|
||||||
"""Test the checkHandle function.
|
"""Test the checkHandle function."""
|
||||||
"""
|
|
||||||
assert checkHandle("None", 1, True) is None
|
assert checkHandle("None", 1, True) is None
|
||||||
assert checkHandle("None", 1, False) == 1
|
assert checkHandle("None", 1, False) == 1
|
||||||
assert checkHandle(None, 1, True) is None
|
assert checkHandle(None, 1, True) is None
|
||||||
@@ -165,8 +163,7 @@ def testBaseCommon_CheckHandle():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_CheckUuid():
|
def testBaseCommon_CheckUuid():
|
||||||
"""Test the checkUuid function.
|
"""Test the checkUuid function."""
|
||||||
"""
|
|
||||||
testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
|
testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
|
||||||
assert checkUuid("", None) is None
|
assert checkUuid("", None) is None
|
||||||
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None
|
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None
|
||||||
@@ -179,8 +176,7 @@ def testBaseCommon_CheckUuid():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_CheckPath():
|
def testBaseCommon_CheckPath():
|
||||||
"""Test the checkPath function.
|
"""Test the checkPath function."""
|
||||||
"""
|
|
||||||
assert checkPath(Path("test"), None) == Path("test")
|
assert checkPath(Path("test"), None) == Path("test")
|
||||||
assert checkPath("test", None) == Path("test")
|
assert checkPath("test", None) == Path("test")
|
||||||
assert checkPath(None, None) is None
|
assert checkPath(None, None) is None
|
||||||
@@ -192,8 +188,7 @@ def testBaseCommon_CheckPath():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_IsHandle():
|
def testBaseCommon_IsHandle():
|
||||||
"""Test the isHandle function.
|
"""Test the isHandle function."""
|
||||||
"""
|
|
||||||
assert isHandle("47666c91c7ccf") is True
|
assert isHandle("47666c91c7ccf") is True
|
||||||
assert isHandle("47666C91C7CCF") is False
|
assert isHandle("47666C91C7CCF") is False
|
||||||
assert isHandle("h7666c91c7ccf") is False
|
assert isHandle("h7666c91c7ccf") is False
|
||||||
@@ -206,8 +201,7 @@ def testBaseCommon_IsHandle():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_IsTitleTag():
|
def testBaseCommon_IsTitleTag():
|
||||||
"""Test the isItemClass function.
|
"""Test the isItemClass function."""
|
||||||
"""
|
|
||||||
assert isTitleTag("T1234") is True
|
assert isTitleTag("T1234") is True
|
||||||
|
|
||||||
assert isTitleTag("t1234") is False
|
assert isTitleTag("t1234") is False
|
||||||
@@ -224,8 +218,7 @@ def testBaseCommon_IsTitleTag():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_IsItemClass():
|
def testBaseCommon_IsItemClass():
|
||||||
"""Test the isItemClass function.
|
"""Test the isItemClass function."""
|
||||||
"""
|
|
||||||
assert isItemClass("NO_CLASS") is True
|
assert isItemClass("NO_CLASS") is True
|
||||||
assert isItemClass("NOVEL") is True
|
assert isItemClass("NOVEL") is True
|
||||||
assert isItemClass("PLOT") is True
|
assert isItemClass("PLOT") is True
|
||||||
@@ -248,8 +241,7 @@ def testBaseCommon_IsItemClass():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_IsItemType():
|
def testBaseCommon_IsItemType():
|
||||||
"""Test the isItemType function.
|
"""Test the isItemType function."""
|
||||||
"""
|
|
||||||
assert isItemType("NO_TYPE") is True
|
assert isItemType("NO_TYPE") is True
|
||||||
assert isItemType("ROOT") is True
|
assert isItemType("ROOT") is True
|
||||||
assert isItemType("FOLDER") is True
|
assert isItemType("FOLDER") is True
|
||||||
@@ -268,8 +260,7 @@ def testBaseCommon_IsItemType():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_IsItemLayout():
|
def testBaseCommon_IsItemLayout():
|
||||||
"""Test the isItemLayout function.
|
"""Test the isItemLayout function."""
|
||||||
"""
|
|
||||||
assert isItemLayout("NO_LAYOUT") is True
|
assert isItemLayout("NO_LAYOUT") is True
|
||||||
assert isItemLayout("DOCUMENT") is True
|
assert isItemLayout("DOCUMENT") is True
|
||||||
assert isItemLayout("NOTE") is True
|
assert isItemLayout("NOTE") is True
|
||||||
@@ -293,8 +284,7 @@ def testBaseCommon_IsItemLayout():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_HexToInt():
|
def testBaseCommon_HexToInt():
|
||||||
"""Test the hexToInt function.
|
"""Test the hexToInt function."""
|
||||||
"""
|
|
||||||
assert hexToInt(1) == 0
|
assert hexToInt(1) == 0
|
||||||
assert hexToInt("1") == 1
|
assert hexToInt("1") == 1
|
||||||
assert hexToInt("0xff") == 255
|
assert hexToInt("0xff") == 255
|
||||||
@@ -307,8 +297,7 @@ def testBaseCommon_HexToInt():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_MinMax():
|
def testBaseCommon_MinMax():
|
||||||
"""Test the minmax function.
|
"""Test the minmax function."""
|
||||||
"""
|
|
||||||
for i in range(-5, 15):
|
for i in range(-5, 15):
|
||||||
assert 0 <= minmax(i, 0, 10) <= 10
|
assert 0 <= minmax(i, 0, 10) <= 10
|
||||||
|
|
||||||
@@ -317,8 +306,7 @@ def testBaseCommon_MinMax():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_CheckIntTuple():
|
def testBaseCommon_CheckIntTuple():
|
||||||
"""Test the checkIntTuple function.
|
"""Test the checkIntTuple function."""
|
||||||
"""
|
|
||||||
assert checkIntTuple(0, (0, 1, 2), 3) == 0
|
assert checkIntTuple(0, (0, 1, 2), 3) == 0
|
||||||
assert checkIntTuple(5, (0, 1, 2), 3) == 3
|
assert checkIntTuple(5, (0, 1, 2), 3) == 3
|
||||||
|
|
||||||
@@ -327,8 +315,7 @@ def testBaseCommon_CheckIntTuple():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_FormatTimeStamp():
|
def testBaseCommon_FormatTimeStamp():
|
||||||
"""Test the formatTimeStamp function.
|
"""Test the formatTimeStamp function."""
|
||||||
"""
|
|
||||||
tTime = time.mktime(time.gmtime(0))
|
tTime = time.mktime(time.gmtime(0))
|
||||||
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
||||||
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
||||||
@@ -338,8 +325,7 @@ def testBaseCommon_FormatTimeStamp():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_FormatTime():
|
def testBaseCommon_FormatTime():
|
||||||
"""Test the formatTime function.
|
"""Test the formatTime function."""
|
||||||
"""
|
|
||||||
assert formatTime("1") == "ERROR"
|
assert formatTime("1") == "ERROR"
|
||||||
assert formatTime(1.0) == "ERROR"
|
assert formatTime(1.0) == "ERROR"
|
||||||
assert formatTime(1) == "00:00:01"
|
assert formatTime(1) == "00:00:01"
|
||||||
@@ -361,8 +347,7 @@ def testBaseCommon_FormatTime():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_Simplified():
|
def testBaseCommon_Simplified():
|
||||||
"""Test the simplified function.
|
"""Test the simplified function."""
|
||||||
"""
|
|
||||||
assert simplified("Hello World") == "Hello World"
|
assert simplified("Hello World") == "Hello World"
|
||||||
assert simplified(" Hello World ") == "Hello World"
|
assert simplified(" Hello World ") == "Hello World"
|
||||||
assert simplified("\tHello\n\r\tWorld") == "Hello World"
|
assert simplified("\tHello\n\r\tWorld") == "Hello World"
|
||||||
@@ -372,8 +357,7 @@ def testBaseCommon_Simplified():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_YesNo():
|
def testBaseCommon_YesNo():
|
||||||
"""Test the yesNo function.
|
"""Test the yesNo function."""
|
||||||
"""
|
|
||||||
# Bool
|
# Bool
|
||||||
assert yesNo(True) == "yes"
|
assert yesNo(True) == "yes"
|
||||||
assert yesNo(False) == "no"
|
assert yesNo(False) == "no"
|
||||||
@@ -400,8 +384,7 @@ def testBaseCommon_YesNo():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_FormatInt():
|
def testBaseCommon_FormatInt():
|
||||||
"""Test the formatInt function.
|
"""Test the formatInt function."""
|
||||||
"""
|
|
||||||
# Normal Cases
|
# Normal Cases
|
||||||
assert formatInt(1) == "1"
|
assert formatInt(1) == "1"
|
||||||
assert formatInt(12) == "12"
|
assert formatInt(12) == "12"
|
||||||
@@ -424,8 +407,7 @@ def testBaseCommon_FormatInt():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_TransferCase():
|
def testBaseCommon_TransferCase():
|
||||||
"""Test the transferCase function.
|
"""Test the transferCase function."""
|
||||||
"""
|
|
||||||
assert transferCase(1, "TaRgEt") == "TaRgEt"
|
assert transferCase(1, "TaRgEt") == "TaRgEt"
|
||||||
assert transferCase("source", 1) == 1
|
assert transferCase("source", 1) == 1
|
||||||
assert transferCase("", "TaRgEt") == "TaRgEt"
|
assert transferCase("", "TaRgEt") == "TaRgEt"
|
||||||
@@ -439,8 +421,7 @@ def testBaseCommon_TransferCase():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_FuzzyTime():
|
def testBaseCommon_FuzzyTime():
|
||||||
"""Test the fuzzyTime function.
|
"""Test the fuzzyTime function."""
|
||||||
"""
|
|
||||||
assert fuzzyTime(-1) == "in the future"
|
assert fuzzyTime(-1) == "in the future"
|
||||||
assert fuzzyTime(0) == "just now"
|
assert fuzzyTime(0) == "just now"
|
||||||
assert fuzzyTime(29) == "just now"
|
assert fuzzyTime(29) == "just now"
|
||||||
@@ -475,8 +456,7 @@ def testBaseCommon_FuzzyTime():
|
|||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testBaseCommon_RomanNumbers():
|
def testBaseCommon_RomanNumbers():
|
||||||
"""Test conversion of integers to Roman numbers.
|
"""Test conversion of integers to Roman numbers."""
|
||||||
"""
|
|
||||||
assert numberToRoman(None, False) == "NAN"
|
assert numberToRoman(None, False) == "NAN"
|
||||||
assert numberToRoman(0, False) == "OOR"
|
assert numberToRoman(0, False) == "OOR"
|
||||||
assert numberToRoman(1, False) == "I"
|
assert numberToRoman(1, False) == "I"
|
||||||
@@ -503,8 +483,7 @@ def testBaseCommon_RomanNumbers():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_JsonEncode():
|
def testBaseCommon_JsonEncode():
|
||||||
"""Test the jsonEncode function.
|
"""Test the jsonEncode function."""
|
||||||
"""
|
|
||||||
# Wrong type
|
# Wrong type
|
||||||
assert jsonEncode(None) == "[]"
|
assert jsonEncode(None) == "[]"
|
||||||
|
|
||||||
@@ -587,8 +566,7 @@ def testBaseCommon_JsonEncode():
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
|
def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
|
||||||
"""Test the readTextFile function.
|
"""Test the readTextFile function."""
|
||||||
"""
|
|
||||||
testText = "\n\n".join(ipsumText) + "\n"
|
testText = "\n\n".join(ipsumText) + "\n"
|
||||||
testFile = fncPath / "ipsum.txt"
|
testFile = fncPath / "ipsum.txt"
|
||||||
writeFile(testFile, testText)
|
writeFile(testFile, testText)
|
||||||
@@ -605,20 +583,20 @@ def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_MakeFileNameSafe():
|
def testBaseCommon_MakeFileNameSafe():
|
||||||
"""Test the makeFileNameSafe function.
|
"""Test the makeFileNameSafe function."""
|
||||||
"""
|
|
||||||
assert makeFileNameSafe(" aaaa ") == "aaaa"
|
assert makeFileNameSafe(" aaaa ") == "aaaa"
|
||||||
assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb"
|
assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb"
|
||||||
assert makeFileNameSafe("aaaa\tbbbb") == "aaaabbbb"
|
assert makeFileNameSafe("aaaa\tbbbb") == "aaaabbbb"
|
||||||
assert makeFileNameSafe("aaaa bbbb") == "aaaa bbbb"
|
assert makeFileNameSafe("aaaa bbbb") == "aaaa bbbb"
|
||||||
|
assert makeFileNameSafe("æøå") == "æøå"
|
||||||
|
assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25"
|
||||||
|
|
||||||
# END Test testBaseCommon_MakeFileNameSafe
|
# END Test testBaseCommon_MakeFileNameSafe
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
|
def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
|
||||||
"""Test the sha256sum function.
|
"""Test the sha256sum function."""
|
||||||
"""
|
|
||||||
longText = 50*(" ".join(ipsumText) + " ")
|
longText = 50*(" ".join(ipsumText) + " ")
|
||||||
shortText = "This is a short file"
|
shortText = "This is a short file"
|
||||||
noneText = ""
|
noneText = ""
|
||||||
@@ -657,8 +635,7 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_GetGuiItem(nwGUI):
|
def testBaseCommon_GetGuiItem(nwGUI):
|
||||||
"""Check the GUI item function.
|
"""Check the GUI item function."""
|
||||||
"""
|
|
||||||
assert getGuiItem("gibberish") is None
|
assert getGuiItem("gibberish") is None
|
||||||
assert isinstance(getGuiItem("GuiMain"), GuiMain)
|
assert isinstance(getGuiItem("GuiMain"), GuiMain)
|
||||||
|
|
||||||
@@ -667,8 +644,7 @@ def testBaseCommon_GetGuiItem(nwGUI):
|
|||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseCommon_NWConfigParser(fncPath):
|
def testBaseCommon_NWConfigParser(fncPath):
|
||||||
"""Test the NWConfigParser subclass.
|
"""Test the NWConfigParser subclass."""
|
||||||
"""
|
|
||||||
tstConf = fncPath / "test.cfg"
|
tstConf = fncPath / "test.cfg"
|
||||||
writeFile(tstConf, (
|
writeFile(tstConf, (
|
||||||
"[main]\n"
|
"[main]\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user