Assemble most of the Manuscript Build tool

This commit is contained in:
Veronica Berglyd Olsen
2023-06-04 20:58:50 +02:00
parent 5f6335d8f9
commit 3996ff8798
18 changed files with 429 additions and 106 deletions
-1
View File
@@ -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

+8 -9
View File
@@ -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:
+10
View File
@@ -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
+59 -1
View File
@@ -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):
+3
View File
@@ -63,6 +63,9 @@ VALID_MAP = {
"GuiManuscript": { "GuiManuscript": {
"winWidth", "winHeight", "optsWidth", "viewWidth" "winWidth", "winHeight", "optsWidth", "viewWidth"
}, },
"GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "optsWidth"
},
} }
+20 -16
View File
@@ -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
+3 -3
View File
@@ -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",
+238 -2
View File
@@ -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
+23 -15
View File
@@ -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")
+35 -59
View File
@@ -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"