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
=======================
Application initialisation
File History:
Created: 2018-09-22 [0.0.1]
@@ -19,6 +19,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
add = typ_plus.svg
backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
browse = typ_folder-open.svg
build_excluded = typ_cancel.svg
build_filtered = typ_filter.svg
build_included = typ_pin.svg
@@ -40,6 +41,7 @@ cls_world = typ_location.svg
cross = typ_times.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
export = typ_export-button.svg
forward = typ_chevron-right.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
@@ -57,6 +59,7 @@ proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
revert = typ_refresh-flipped.svg
search = typ_search.svg
search_cancel = typ_cancel-grey.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
backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
browse = typ_folder-open.svg
build_excluded = typ_cancel.svg
build_filtered = typ_filter.svg
build_included = typ_pin.svg
@@ -40,6 +41,7 @@ cls_world = typ_location.svg
cross = typ_times.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
export = typ_export-button.svg
forward = typ_chevron-right.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
@@ -57,6 +59,7 @@ proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
revert = typ_refresh-flipped.svg
search = typ_search.svg
search_cancel = typ_cancel-grey.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 hashlib
import logging
import unicodedata
import xml.etree.ElementTree as ET
from typing import Any, Literal
@@ -469,8 +470,7 @@ def xmlIndent(tree: ET.Element | ET.ElementTree):
# =============================================================================================== #
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)
if not path.is_file():
return ""
@@ -482,14 +482,13 @@ def readTextFile(path: str | Path) -> str:
return ""
def makeFileNameSafe(value: str) -> str:
"""Returns a filename safe string of the value.
def makeFileNameSafe(text: str) -> str:
"""Return a filename safe string.
See: https://unicode.org/reports/tr15/#Norm_Forms
"""
clean = ""
for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ":
clean += c
return clean
text = unicodedata.normalize("NFKC", text).strip()
allowed = (" ", ".", "-", "_")
return "".join(c for c in text if c.isalnum() or c in allowed)
def sha256sum(path: str | Path) -> str | None:
+10
View File
@@ -199,6 +199,16 @@ class nwLabels:
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
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
+59 -1
View File
@@ -35,7 +35,7 @@ from pathlib import Path
from PyQt5.QtCore import QT_TRANSLATE_NOOP
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.project import NWProject
from novelwriter.error import logException
@@ -72,6 +72,9 @@ SETTINGS_TEMPLATE = {
"format.replaceTabs": (bool, False),
"odt.addColours": (bool, True),
"html.addStyles": (bool, False),
"build.splitNovel": (bool, False),
"build.splitNotes": (bool, False),
"build.splitChapters": (bool, False),
}
SETTINGS_LABELS = {
@@ -112,6 +115,11 @@ SETTINGS_LABELS = {
"html": QT_TRANSLATE_NOOP("Builds", "HTML"),
"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):
self._name = ""
self._uuid = str(uuid.uuid4())
self._path = Path.home()
self._build = ""
self._format = "odt"
self._skipRoot = set()
self._excluded = set()
self._included = set()
@@ -158,6 +169,23 @@ class BuildSettings:
"""The build ID as an 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
def changed(self) -> bool:
"""The changed status of the build."""
@@ -214,6 +242,30 @@ class BuildSettings:
self._uuid = value
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):
"""Set an item as filtered."""
self._excluded.discard(tHandle)
@@ -325,6 +377,9 @@ class BuildSettings:
return {
"name": self._name,
"uuid": self._uuid,
"path": str(self._path),
"build": self._build,
"format": self._format,
"settings": self._settings.copy(),
"content": {
"included": list(self._included),
@@ -343,6 +398,9 @@ class BuildSettings:
self.setName(data.get("name", ""))
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):
self._included = set([h for h in included if isHandle(h)])
if isinstance(excluded, list):
+3
View File
@@ -63,6 +63,9 @@ VALID_MAP = {
"GuiManuscript": {
"winWidth", "winHeight", "optsWidth", "viewWidth"
},
"GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "optsWidth"
},
}
+20 -16
View File
@@ -1,7 +1,6 @@
"""
novelWriter Custom Widget: Switch Box
=======================================
A box of icons, labels and switches
File History:
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
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QGridLayout, QLabel, QScrollArea, QSizePolicy, QWidget
from novelwriter.extensions.switch import NSwitch
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)
def __init__(self, parent, baseSize):
def __init__(self, parent: QWidget, baseSize: int):
super().__init__(parent=parent)
self._index = 0
self._hSwitch = baseSize
self._wSwitch = 2*self._hSwitch
self._sIcon = baseSize
self.clear()
return
def clear(self):
"""Rebuild the content of the core widget.
"""
"""Rebuild the content of the core widget."""
self._content = QGridLayout()
self._content.setColumnStretch(1, 1)
@@ -60,9 +62,8 @@ class NSwitchBox(QScrollArea):
return
def addLabel(self, text):
"""Add a header label to the content box.
"""
def addLabel(self, text: str):
"""Add a header label to the content box."""
label = QLabel(text)
font = label.font()
font.setBold(True)
@@ -71,9 +72,8 @@ class NSwitchBox(QScrollArea):
self._bumpIndex()
return
def addItem(self, qIcon, text, identifier, default=False):
"""Add an item to the content box.
"""
def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False):
"""Add an item to the content box."""
icon = QLabel("")
icon.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
icon.setPixmap(qIcon.pixmap(self._sIcon, self._sIcon))
@@ -100,13 +100,17 @@ class NSwitchBox(QScrollArea):
self._bumpIndex()
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
##
def _emitSwitchSignal(self, identifier, state):
"""Emit a signal for a switch toggle.
"""
def _emitSwitchSignal(self, identifier: str, state: bool):
"""Emit a signal for a switch toggle."""
self.switchToggled.emit(identifier, state)
return
+3 -3
View File
@@ -454,9 +454,9 @@ class GuiIcons:
"view_novel", "view_outline",
# General Button Icons
"add", "backward", "bookmark", "checked", "close", "cross", "down", "edit", "forward",
"maximise", "menu", "minimise", "noncheckable", "reference", "refresh", "remove",
"search_replace", "search", "settings", "unchecked", "up",
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
"export", "forward", "maximise", "menu", "minimise", "noncheckable", "reference",
"refresh", "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
# Switches
"sticky-on", "sticky-off",
+238 -2
View File
@@ -25,7 +25,23 @@ from __future__ import annotations
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__)
@@ -37,13 +53,146 @@ class GuiManuscriptBuild(QDialog):
independently of the Manuscript Build Tool.
"""
def __init__(self, parent: QWidget):
def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings):
super().__init__(parent=parent)
logger.debug("Create: GuiManuscriptBuild")
self.setObjectName("GuiManuscriptBuild")
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
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")
@@ -54,4 +203,91 @@ class GuiManuscriptBuild(QDialog):
logger.debug("Delete: GuiManuscriptBuild")
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
+23 -15
View File
@@ -44,6 +44,7 @@ from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
if TYPE_CHECKING: # pragma: no cover
@@ -107,26 +108,16 @@ class GuiManuscript(QDialog):
self.btnPreview.clicked.connect(self._generatePreview)
self.manPreview = _PreviewWidget(self.mainGui)
self.btnBuild = QPushButton(self.tr("Build"))
self.btnBuild.clicked.connect(self._buildManuscript)
self.menuPrint = QMenu(self)
self.aPrintSend = self.menuPrint.addAction(self.tr("Print Preview"))
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.setMenu(self.menuPrint)
self.btnSave = QPushButton(self.tr("Save As"))
self.btnSave.setMenu(self.menuSave)
self.btnClose = QPushButton(self.tr("Close"))
self.btnClose.clicked.connect(self._doClose)
@@ -139,7 +130,7 @@ class GuiManuscript(QDialog):
self.buildBox.addWidget(self.btnDelete)
self.processBox = QHBoxLayout()
self.processBox.addWidget(self.btnSave)
self.processBox.addWidget(self.btnBuild)
self.processBox.addWidget(self.btnPrint)
self.processBox.addWidget(self.btnClose)
@@ -283,6 +274,23 @@ class GuiManuscript(QDialog):
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()
def _doClose(self):
"""Forward the close button to the default close method."""
@@ -514,7 +522,7 @@ class _PreviewWidget(QTextBrowser):
if self._docTime > 0:
strBuildTime = "%s (%s)" % (
datetime.fromtimestamp(self._docTime).strftime("%x %X"),
fuzzyTime(time() - self._docTime)
fuzzyTime(int(time()) - self._docTime)
)
else:
strBuildTime = self.tr("Unknown")
+35 -59
View File
@@ -25,24 +25,23 @@ import hashlib
from pathlib import Path
from mocked import causeOSError
from tools import writeFile
from mocked import causeOSError
from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkStringNone, checkString, checkInt, checkFloat, checkBool, checkHandle,
checkUuid, checkPath, isHandle, isTitleTag, isItemClass, isItemType,
isItemLayout, hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp,
formatTime, simplified, yesNo, transferCase, fuzzyTime, numberToRoman,
jsonEncode, readTextFile, makeFileNameSafe, sha256sum, getGuiItem,
NWConfigParser
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, formatInt, formatTime,
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified,
transferCase, yesNo
)
@pytest.mark.base
def testBaseCommon_CheckStringNone():
"""Test the checkStringNone function.
"""
"""Test the checkStringNone function."""
assert checkStringNone("Stuff", "NotNone") == "Stuff"
assert checkStringNone("None", "NotNone") is None
assert checkStringNone(None, "NotNone") is None
@@ -151,8 +150,7 @@ def testBaseCommon_CheckBool():
@pytest.mark.base
def testBaseCommon_CheckHandle():
"""Test the checkHandle function.
"""
"""Test the checkHandle function."""
assert checkHandle("None", 1, True) is None
assert checkHandle("None", 1, False) == 1
assert checkHandle(None, 1, True) is None
@@ -165,8 +163,7 @@ def testBaseCommon_CheckHandle():
@pytest.mark.base
def testBaseCommon_CheckUuid():
"""Test the checkUuid function.
"""
"""Test the checkUuid function."""
testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
assert checkUuid("", None) is None
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None
@@ -179,8 +176,7 @@ def testBaseCommon_CheckUuid():
@pytest.mark.base
def testBaseCommon_CheckPath():
"""Test the checkPath function.
"""
"""Test the checkPath function."""
assert checkPath(Path("test"), None) == Path("test")
assert checkPath("test", None) == Path("test")
assert checkPath(None, None) is None
@@ -192,8 +188,7 @@ def testBaseCommon_CheckPath():
@pytest.mark.base
def testBaseCommon_IsHandle():
"""Test the isHandle function.
"""
"""Test the isHandle function."""
assert isHandle("47666c91c7ccf") is True
assert isHandle("47666C91C7CCF") is False
assert isHandle("h7666c91c7ccf") is False
@@ -206,8 +201,7 @@ def testBaseCommon_IsHandle():
@pytest.mark.base
def testBaseCommon_IsTitleTag():
"""Test the isItemClass function.
"""
"""Test the isItemClass function."""
assert isTitleTag("T1234") is True
assert isTitleTag("t1234") is False
@@ -224,8 +218,7 @@ def testBaseCommon_IsTitleTag():
@pytest.mark.base
def testBaseCommon_IsItemClass():
"""Test the isItemClass function.
"""
"""Test the isItemClass function."""
assert isItemClass("NO_CLASS") is True
assert isItemClass("NOVEL") is True
assert isItemClass("PLOT") is True
@@ -248,8 +241,7 @@ def testBaseCommon_IsItemClass():
@pytest.mark.base
def testBaseCommon_IsItemType():
"""Test the isItemType function.
"""
"""Test the isItemType function."""
assert isItemType("NO_TYPE") is True
assert isItemType("ROOT") is True
assert isItemType("FOLDER") is True
@@ -268,8 +260,7 @@ def testBaseCommon_IsItemType():
@pytest.mark.base
def testBaseCommon_IsItemLayout():
"""Test the isItemLayout function.
"""
"""Test the isItemLayout function."""
assert isItemLayout("NO_LAYOUT") is True
assert isItemLayout("DOCUMENT") is True
assert isItemLayout("NOTE") is True
@@ -293,8 +284,7 @@ def testBaseCommon_IsItemLayout():
@pytest.mark.base
def testBaseCommon_HexToInt():
"""Test the hexToInt function.
"""
"""Test the hexToInt function."""
assert hexToInt(1) == 0
assert hexToInt("1") == 1
assert hexToInt("0xff") == 255
@@ -307,8 +297,7 @@ def testBaseCommon_HexToInt():
@pytest.mark.base
def testBaseCommon_MinMax():
"""Test the minmax function.
"""
"""Test the minmax function."""
for i in range(-5, 15):
assert 0 <= minmax(i, 0, 10) <= 10
@@ -317,8 +306,7 @@ def testBaseCommon_MinMax():
@pytest.mark.base
def testBaseCommon_CheckIntTuple():
"""Test the checkIntTuple function.
"""
"""Test the checkIntTuple function."""
assert checkIntTuple(0, (0, 1, 2), 3) == 0
assert checkIntTuple(5, (0, 1, 2), 3) == 3
@@ -327,8 +315,7 @@ def testBaseCommon_CheckIntTuple():
@pytest.mark.base
def testBaseCommon_FormatTimeStamp():
"""Test the formatTimeStamp function.
"""
"""Test the formatTimeStamp function."""
tTime = time.mktime(time.gmtime(0))
assert formatTimeStamp(tTime, False) == "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
def testBaseCommon_FormatTime():
"""Test the formatTime function.
"""
"""Test the formatTime function."""
assert formatTime("1") == "ERROR"
assert formatTime(1.0) == "ERROR"
assert formatTime(1) == "00:00:01"
@@ -361,8 +347,7 @@ def testBaseCommon_FormatTime():
@pytest.mark.base
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("\tHello\n\r\tWorld") == "Hello World"
@@ -372,8 +357,7 @@ def testBaseCommon_Simplified():
@pytest.mark.base
def testBaseCommon_YesNo():
"""Test the yesNo function.
"""
"""Test the yesNo function."""
# Bool
assert yesNo(True) == "yes"
assert yesNo(False) == "no"
@@ -400,8 +384,7 @@ def testBaseCommon_YesNo():
@pytest.mark.base
def testBaseCommon_FormatInt():
"""Test the formatInt function.
"""
"""Test the formatInt function."""
# Normal Cases
assert formatInt(1) == "1"
assert formatInt(12) == "12"
@@ -424,8 +407,7 @@ def testBaseCommon_FormatInt():
@pytest.mark.base
def testBaseCommon_TransferCase():
"""Test the transferCase function.
"""
"""Test the transferCase function."""
assert transferCase(1, "TaRgEt") == "TaRgEt"
assert transferCase("source", 1) == 1
assert transferCase("", "TaRgEt") == "TaRgEt"
@@ -439,8 +421,7 @@ def testBaseCommon_TransferCase():
@pytest.mark.base
def testBaseCommon_FuzzyTime():
"""Test the fuzzyTime function.
"""
"""Test the fuzzyTime function."""
assert fuzzyTime(-1) == "in the future"
assert fuzzyTime(0) == "just now"
assert fuzzyTime(29) == "just now"
@@ -475,8 +456,7 @@ def testBaseCommon_FuzzyTime():
@pytest.mark.core
def testBaseCommon_RomanNumbers():
"""Test conversion of integers to Roman numbers.
"""
"""Test conversion of integers to Roman numbers."""
assert numberToRoman(None, False) == "NAN"
assert numberToRoman(0, False) == "OOR"
assert numberToRoman(1, False) == "I"
@@ -503,8 +483,7 @@ def testBaseCommon_RomanNumbers():
@pytest.mark.base
def testBaseCommon_JsonEncode():
"""Test the jsonEncode function.
"""
"""Test the jsonEncode function."""
# Wrong type
assert jsonEncode(None) == "[]"
@@ -587,8 +566,7 @@ def testBaseCommon_JsonEncode():
@pytest.mark.base
def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
"""Test the readTextFile function.
"""
"""Test the readTextFile function."""
testText = "\n\n".join(ipsumText) + "\n"
testFile = fncPath / "ipsum.txt"
writeFile(testFile, testText)
@@ -605,20 +583,20 @@ def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
@pytest.mark.base
def testBaseCommon_MakeFileNameSafe():
"""Test the makeFileNameSafe function.
"""
"""Test the makeFileNameSafe function."""
assert makeFileNameSafe(" aaaa ") == "aaaa"
assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb"
assert makeFileNameSafe("aaaa\tbbbb") == "aaaabbbb"
assert makeFileNameSafe("aaaa bbbb") == "aaaa bbbb"
assert makeFileNameSafe("æøå") == "æøå"
assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25"
# END Test testBaseCommon_MakeFileNameSafe
@pytest.mark.base
def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
"""Test the sha256sum function.
"""
"""Test the sha256sum function."""
longText = 50*(" ".join(ipsumText) + " ")
shortText = "This is a short file"
noneText = ""
@@ -657,8 +635,7 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
@pytest.mark.base
def testBaseCommon_GetGuiItem(nwGUI):
"""Check the GUI item function.
"""
"""Check the GUI item function."""
assert getGuiItem("gibberish") is None
assert isinstance(getGuiItem("GuiMain"), GuiMain)
@@ -667,8 +644,7 @@ def testBaseCommon_GetGuiItem(nwGUI):
@pytest.mark.base
def testBaseCommon_NWConfigParser(fncPath):
"""Test the NWConfigParser subclass.
"""
"""Test the NWConfigParser subclass."""
tstConf = fncPath / "test.cfg"
writeFile(tstConf, (
"[main]\n"