Merge branch 'main' into feature/status_shapes
This commit is contained in:
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
|
|||||||
__author__ = "Veronica Berglyd Olsen"
|
__author__ = "Veronica Berglyd Olsen"
|
||||||
__maintainer__ = "Veronica Berglyd Olsen"
|
__maintainer__ = "Veronica Berglyd Olsen"
|
||||||
__email__ = "code@vkbo.net"
|
__email__ = "code@vkbo.net"
|
||||||
__version__ = "2.4rc1"
|
__version__ = "2.5rc1"
|
||||||
__hexversion__ = "0x020400c1"
|
__hexversion__ = "0x020500a1"
|
||||||
__date__ = "2024-04-06"
|
__date__ = "2024-04-13"
|
||||||
__status__ = "Stable"
|
__status__ = "Stable"
|
||||||
__domain__ = "novelwriter.io"
|
__domain__ = "novelwriter.io"
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,10 @@ class Config:
|
|||||||
"""Un-scale fixed gui sizes by the screen scale factor."""
|
"""Un-scale fixed gui sizes by the screen scale factor."""
|
||||||
return int(value/self.guiScale)
|
return int(value/self.guiScale)
|
||||||
|
|
||||||
|
def homePath(self) -> Path:
|
||||||
|
"""The user's home folder."""
|
||||||
|
return self._homePath
|
||||||
|
|
||||||
def dataPath(self, target: str | None = None) -> Path:
|
def dataPath(self, target: str | None = None) -> Path:
|
||||||
"""Return a path in the data folder."""
|
"""Return a path in the data folder."""
|
||||||
if isinstance(target, str):
|
if isinstance(target, str):
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class BuildSettings:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._uuid = str(uuid.uuid4())
|
self._uuid = str(uuid.uuid4())
|
||||||
self._path = Path.home()
|
self._path = CONFIG.homePath()
|
||||||
self._build = ""
|
self._build = ""
|
||||||
self._order = 0
|
self._order = 0
|
||||||
self._format = nwBuildFmt.ODT
|
self._format = nwBuildFmt.ODT
|
||||||
@@ -220,7 +220,7 @@ class BuildSettings:
|
|||||||
"""The last used build path."""
|
"""The last used build path."""
|
||||||
if self._path.is_dir():
|
if self._path.is_dir():
|
||||||
return self._path
|
return self._path
|
||||||
return Path.home()
|
return CONFIG.homePath()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lastBuildName(self) -> str:
|
def lastBuildName(self) -> str:
|
||||||
@@ -297,7 +297,7 @@ class BuildSettings:
|
|||||||
if isinstance(path, Path) and path.is_dir():
|
if isinstance(path, Path) and path.is_dir():
|
||||||
self._path = path
|
self._path = path
|
||||||
else:
|
else:
|
||||||
self._path = Path.home()
|
self._path = CONFIG.homePath()
|
||||||
self._changed = True
|
self._changed = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -263,17 +263,17 @@ class _SettingsPage(NScrollableForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Project Language
|
# Project Language
|
||||||
|
projLang = data.language or CONFIG.guiLocale
|
||||||
self.projLang = NComboBox(self)
|
self.projLang = NComboBox(self)
|
||||||
self.projLang.setMinimumWidth(xW)
|
self.projLang.setMinimumWidth(xW)
|
||||||
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
|
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
|
||||||
self.projLang.addItem(language, tag)
|
self.projLang.addItem(language, tag)
|
||||||
|
self.projLang.setCurrentData(projLang, projLang)
|
||||||
self.addRow(
|
self.addRow(
|
||||||
self.tr("Project language"), self.projLang,
|
self.tr("Project language"), self.projLang,
|
||||||
self.tr("Only used when building the manuscript."),
|
self.tr("Only used when building the manuscript."),
|
||||||
stretch=(3, 2)
|
stretch=(3, 2)
|
||||||
)
|
)
|
||||||
if (idx := self.projLang.findData(data.language)) != -1:
|
|
||||||
self.projLang.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
# Spell Check Language
|
# Spell Check Language
|
||||||
self.spellLang = NComboBox(self)
|
self.spellLang = NComboBox(self)
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class GuiWordList(QDialog):
|
|||||||
))
|
))
|
||||||
ffilter = formatFileFilter(["*.txt", "*"])
|
ffilter = formatFileFilter(["*.txt", "*"])
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
self, self.tr("Import File"), str(Path.home()), filter=ffilter
|
self, self.tr("Import File"), str(CONFIG.homePath()), filter=ffilter
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
try:
|
try:
|
||||||
@@ -207,7 +207,7 @@ class GuiWordList(QDialog):
|
|||||||
def _exportWords(self) -> None:
|
def _exportWords(self) -> None:
|
||||||
"""Export words to file."""
|
"""Export words to file."""
|
||||||
path, _ = QFileDialog.getSaveFileName(
|
path, _ = QFileDialog.getSaveFileName(
|
||||||
self, self.tr("Export File"), str(Path.home())
|
self, self.tr("Export File"), str(CONFIG.homePath())
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
|
|||||||
continue
|
continue
|
||||||
if line[0] == ">":
|
if line[0] == ">":
|
||||||
line = line.lstrip(">").lstrip(" ")
|
line = line.lstrip(">").lstrip(" ")
|
||||||
|
if line: # Above block can return empty line (Issue #1816)
|
||||||
if line[-1] == "<":
|
if line[-1] == "<":
|
||||||
line = line.rstrip("<").rstrip(" ")
|
line = line.rstrip("<").rstrip(" ")
|
||||||
if "[" in line:
|
if "[" in line:
|
||||||
|
|||||||
@@ -303,25 +303,21 @@ class GuiManuscript(QDialog):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _editSelectedBuild(self) -> None:
|
def _editSelectedBuild(self) -> None:
|
||||||
"""Edit the currently selected build settings entry."""
|
"""Edit the currently selected build settings entry."""
|
||||||
build = self._getSelectedBuild()
|
if build := self._getSelectedBuild():
|
||||||
if build is not None:
|
|
||||||
self._openSettingsDialog(build)
|
self._openSettingsDialog(build)
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
|
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
|
||||||
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
|
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
|
||||||
"""Process change of build selection to update the details."""
|
"""Process change of build selection to update the details."""
|
||||||
if isinstance(current, QListWidgetItem):
|
if current and (build := self._builds.getBuild(current.data(self.D_KEY))):
|
||||||
build = self._builds.getBuild(current.data(self.D_KEY))
|
self.buildDetails.updateInfo(build)
|
||||||
if build is not None:
|
|
||||||
self.buildDetails.updateInfo(build)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _deleteSelectedBuild(self) -> None:
|
def _deleteSelectedBuild(self) -> None:
|
||||||
"""Delete the currently selected build settings entry."""
|
"""Delete the currently selected build settings entry."""
|
||||||
build = self._getSelectedBuild()
|
if build := self._getSelectedBuild():
|
||||||
if build is not None:
|
|
||||||
if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))):
|
if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))):
|
||||||
self._builds.removeBuild(build.buildID)
|
self._builds.removeBuild(build.buildID)
|
||||||
self._updateBuildsList()
|
self._updateBuildsList()
|
||||||
@@ -332,8 +328,7 @@ class GuiManuscript(QDialog):
|
|||||||
"""Process new build settings from the settings dialog."""
|
"""Process new build settings from the settings dialog."""
|
||||||
self._builds.setBuild(build)
|
self._builds.setBuild(build)
|
||||||
self._updateBuildItem(build)
|
self._updateBuildItem(build)
|
||||||
current = self.buildList.currentItem()
|
if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID:
|
||||||
if isinstance(current, QListWidgetItem) and current.data(self.D_KEY) == build.buildID:
|
|
||||||
self._updateBuildDetails(current, current)
|
self._updateBuildDetails(current, current)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -342,8 +337,7 @@ class GuiManuscript(QDialog):
|
|||||||
"""Run the document builder on the current build settings for
|
"""Run the document builder on the current build settings for
|
||||||
the preview widget.
|
the preview widget.
|
||||||
"""
|
"""
|
||||||
build = self._getSelectedBuild()
|
if not (build := self._getSelectedBuild()):
|
||||||
if build is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
docBuild = NWBuildDocument(SHARED.project, build)
|
docBuild = NWBuildDocument(SHARED.project, build)
|
||||||
@@ -383,8 +377,7 @@ class GuiManuscript(QDialog):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _buildManuscript(self) -> None:
|
def _buildManuscript(self) -> None:
|
||||||
"""Open the build dialog and build the manuscript."""
|
"""Open the build dialog and build the manuscript."""
|
||||||
build = self._getSelectedBuild()
|
if build := self._getSelectedBuild():
|
||||||
if isinstance(build, BuildSettings):
|
|
||||||
dlgBuild = GuiManuscriptBuild(self, build)
|
dlgBuild = GuiManuscriptBuild(self, build)
|
||||||
dlgBuild.exec()
|
dlgBuild.exec()
|
||||||
|
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ class _NewProjectForm(QWidget):
|
|||||||
def __init__(self, parent: QWidget) -> None:
|
def __init__(self, parent: QWidget) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self._basePath = CONFIG.lastPath()
|
self._basePath = CONFIG.homePath()
|
||||||
self._fillMode = self.FILL_BLANK
|
self._fillMode = self.FILL_BLANK
|
||||||
self._copyPath = None
|
self._copyPath = None
|
||||||
|
|
||||||
@@ -632,7 +632,7 @@ class _NewProjectForm(QWidget):
|
|||||||
|
|
||||||
self.numChapters = NSpinBox(self)
|
self.numChapters = NSpinBox(self)
|
||||||
self.numChapters.setRange(0, 200)
|
self.numChapters.setRange(0, 200)
|
||||||
self.numChapters.setValue(5)
|
self.numChapters.setValue(0)
|
||||||
self.numChapters.setToolTip(self.tr("Set to 0 to only add scenes"))
|
self.numChapters.setToolTip(self.tr("Set to 0 to only add scenes"))
|
||||||
|
|
||||||
self.chapterBox = NWrappedWidgetBox(
|
self.chapterBox = NWrappedWidgetBox(
|
||||||
@@ -642,7 +642,7 @@ class _NewProjectForm(QWidget):
|
|||||||
|
|
||||||
self.numScenes = NSpinBox(self)
|
self.numScenes = NSpinBox(self)
|
||||||
self.numScenes.setRange(0, 200)
|
self.numScenes.setRange(0, 200)
|
||||||
self.numScenes.setValue(5)
|
self.numScenes.setValue(0)
|
||||||
|
|
||||||
self.sceneBox = NWrappedWidgetBox(
|
self.sceneBox = NWrappedWidgetBox(
|
||||||
self.tr("Add {0} scene documents (to each chapter)"), self.numScenes
|
self.tr("Add {0} scene documents (to each chapter)"), self.numScenes
|
||||||
@@ -742,7 +742,6 @@ class _NewProjectForm(QWidget):
|
|||||||
):
|
):
|
||||||
self._basePath = Path(projDir)
|
self._basePath = Path(projDir)
|
||||||
self._updateProjPath()
|
self._updateProjPath()
|
||||||
CONFIG.setLastPath(self._basePath)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ def testBaseConfig_Methods(fncPath):
|
|||||||
tstConf = Config()
|
tstConf = Config()
|
||||||
tstConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
tstConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
||||||
|
|
||||||
|
# Home Path
|
||||||
|
assert tstConf.homePath() == Path.home().absolute()
|
||||||
|
|
||||||
# Data Path
|
# Data Path
|
||||||
assert tstConf.dataPath() == fncPath
|
assert tstConf.dataPath() == fncPath
|
||||||
assert tstConf.dataPath("stuff") == fncPath / "stuff"
|
assert tstConf.dataPath("stuff") == fncPath / "stuff"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import shutil
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from novelwriter import CONFIG
|
||||||
from tools import C, buildTestProject
|
from tools import C, buildTestProject
|
||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ def testCoreBuildSettings_ClassAttributes(fncPath: Path):
|
|||||||
build.setName("Test Build")
|
build.setName("Test Build")
|
||||||
assert build.name == "Test Build"
|
assert build.name == "Test Build"
|
||||||
|
|
||||||
# Only valid UUIDs are accpeted, anything else generates a new UUID
|
# Only valid UUIDs are accepted, anything else generates a new UUID
|
||||||
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
|
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
|
||||||
assert build.buildID == "5cf45d24-f496-42c9-8733-529a9e52a62b"
|
assert build.buildID == "5cf45d24-f496-42c9-8733-529a9e52a62b"
|
||||||
|
|
||||||
@@ -72,14 +73,14 @@ def testCoreBuildSettings_ClassAttributes(fncPath: Path):
|
|||||||
|
|
||||||
# Last path must be valid, if not it defaults to $HOME
|
# Last path must be valid, if not it defaults to $HOME
|
||||||
build.setLastPath("/path/to/nowhere")
|
build.setLastPath("/path/to/nowhere")
|
||||||
assert build.lastPath == Path.home()
|
assert build.lastPath == CONFIG.homePath()
|
||||||
|
|
||||||
build.setLastPath(None)
|
build.setLastPath(None)
|
||||||
assert build.lastPath == Path.home()
|
assert build.lastPath == CONFIG.homePath()
|
||||||
|
|
||||||
(fncPath / "test.txt").write_text("foobar")
|
(fncPath / "test.txt").write_text("foobar")
|
||||||
build.setLastPath(fncPath / "test.txt") # Can't be a file
|
build.setLastPath(fncPath / "test.txt") # Can't be a file
|
||||||
assert build.lastPath == Path.home()
|
assert build.lastPath == CONFIG.homePath()
|
||||||
|
|
||||||
build.setLastPath(fncPath)
|
build.setLastPath(fncPath)
|
||||||
assert build.lastPath == fncPath
|
assert build.lastPath == fncPath
|
||||||
@@ -93,7 +94,7 @@ def testCoreBuildSettings_ClassAttributes(fncPath: Path):
|
|||||||
build.setLastPath(testDir)
|
build.setLastPath(testDir)
|
||||||
assert build.lastPath == testDir
|
assert build.lastPath == testDir
|
||||||
testDir.rmdir()
|
testDir.rmdir()
|
||||||
assert build.lastPath == Path.home()
|
assert build.lastPath == CONFIG.homePath()
|
||||||
|
|
||||||
# Last build name
|
# Last build name
|
||||||
build.setLastBuildName(None) # type: ignore
|
build.setLastBuildName(None) # type: ignore
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from novelwriter.text.counting import bodyTextCounter, preProcessText, standardC
|
|||||||
def testTextCounting_preProcessText():
|
def testTextCounting_preProcessText():
|
||||||
"""Test the text preprocessor for counters."""
|
"""Test the text preprocessor for counters."""
|
||||||
# Not Text
|
# Not Text
|
||||||
assert preProcessText(None) == []
|
assert preProcessText(None) == [] # type: ignore
|
||||||
|
|
||||||
# No Text
|
# No Text
|
||||||
assert preProcessText("") == []
|
assert preProcessText("") == []
|
||||||
@@ -81,6 +81,10 @@ def testTextCounting_standardCounter():
|
|||||||
assert standardCounter(None) == (0, 0, 0) # type: ignore
|
assert standardCounter(None) == (0, 0, 0) # type: ignore
|
||||||
assert standardCounter(1234) == (0, 0, 0) # type: ignore
|
assert standardCounter(1234) == (0, 0, 0) # type: ignore
|
||||||
|
|
||||||
|
# Test Corner Cases, Bug #1816
|
||||||
|
assert standardCounter("> ") == (0, 0, 0)
|
||||||
|
assert standardCounter(" <") == (0, 0, 0)
|
||||||
|
|
||||||
# General Text
|
# General Text
|
||||||
cC, wC, pC = standardCounter((
|
cC, wC, pC = standardCounter((
|
||||||
"#! Title\n\n"
|
"#! Title\n\n"
|
||||||
@@ -88,7 +92,8 @@ def testTextCounting_standardCounter():
|
|||||||
"# Heading One\n"
|
"# Heading One\n"
|
||||||
"## Heading Two\n"
|
"## Heading Two\n"
|
||||||
"### Heading Three\n"
|
"### Heading Three\n"
|
||||||
"#### Heading Four\n\n"
|
"###! Heading Four\n"
|
||||||
|
"#### Heading Five\n\n"
|
||||||
"@tag: value\n\n"
|
"@tag: value\n\n"
|
||||||
"% A comment that should not be counted.\n\n"
|
"% A comment that should not be counted.\n\n"
|
||||||
"The first paragraph.\n\n"
|
"The first paragraph.\n\n"
|
||||||
@@ -96,8 +101,8 @@ def testTextCounting_standardCounter():
|
|||||||
"The third paragraph.\n\n"
|
"The third paragraph.\n\n"
|
||||||
"Dashes\u2013and even longer\u2014dashes."
|
"Dashes\u2013and even longer\u2014dashes."
|
||||||
))
|
))
|
||||||
assert cC == 151
|
assert cC == 163
|
||||||
assert wC == 24
|
assert wC == 26
|
||||||
assert pC == 4
|
assert pC == 4
|
||||||
|
|
||||||
# Text Alignment
|
# Text Alignment
|
||||||
@@ -182,7 +187,7 @@ def testTextCounting_standardCounter():
|
|||||||
def testTextCounting_bodyTextCounter():
|
def testTextCounting_bodyTextCounter():
|
||||||
"""Test the body text counter."""
|
"""Test the body text counter."""
|
||||||
# Not Text
|
# Not Text
|
||||||
assert bodyTextCounter(None) == (0, 0, 0)
|
assert bodyTextCounter(None) == (0, 0, 0) # type: ignore
|
||||||
|
|
||||||
# General Text
|
# General Text
|
||||||
wC, cC, sC = bodyTextCounter((
|
wC, cC, sC = bodyTextCounter((
|
||||||
|
|||||||
Reference in New Issue
Block a user