From f62b755903feb69e5066eac0e21a27e76f458e58 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 5 Nov 2024 18:17:21 +0100
Subject: [PATCH 1/3] Add copy button icon
---
novelwriter/assets/icons/typicons_dark/icons.conf | 1 +
novelwriter/assets/icons/typicons_dark/mixed_copy.svg | 4 ++++
novelwriter/assets/icons/typicons_light/icons.conf | 1 +
novelwriter/assets/icons/typicons_light/mixed_copy.svg | 4 ++++
novelwriter/gui/theme.py | 9 +++++----
5 files changed, 15 insertions(+), 4 deletions(-)
create mode 100644 novelwriter/assets/icons/typicons_dark/mixed_copy.svg
create mode 100644 novelwriter/assets/icons/typicons_light/mixed_copy.svg
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index a4f8cf166..b70c1da51 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -44,6 +44,7 @@ cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
+copy = mixed_copy.svg
cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg
diff --git a/novelwriter/assets/icons/typicons_dark/mixed_copy.svg b/novelwriter/assets/icons/typicons_dark/mixed_copy.svg
new file mode 100644
index 000000000..ff43bce52
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/mixed_copy.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 022f417ea..cf18d9660 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -44,6 +44,7 @@ cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
+copy = mixed_copy.svg
cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg
diff --git a/novelwriter/assets/icons/typicons_light/mixed_copy.svg b/novelwriter/assets/icons/typicons_light/mixed_copy.svg
new file mode 100644
index 000000000..17b6efc72
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/mixed_copy.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 4576255a6..340951cfa 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -503,10 +503,11 @@ class GuiIcons:
"fmt_strike-md", "fmt_subscript", "fmt_superscript", "fmt_underline",
# General Button Icons
- "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
- "document", "down", "edit", "export", "font", "forward", "import", "list", "maximise",
- "menu", "minimise", "more", "noncheckable", "open", "panel", "quote", "refresh", "remove",
- "revert", "search_replace", "search", "settings", "star", "unchecked", "up", "view",
+ "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "copy",
+ "cross", "document", "down", "edit", "export", "font", "forward", "import", "list",
+ "maximise", "menu", "minimise", "more", "noncheckable", "open", "panel", "quote",
+ "refresh", "remove", "revert", "search_replace", "search", "settings", "star", "unchecked",
+ "up", "view",
# Switches
"sticky-on", "sticky-off",
From 0b59ee863445d8082c57f4bcce0ae08a786c29b1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 5 Nov 2024 18:17:43 +0100
Subject: [PATCH 2/3] Add duplicate build feature
---
novelwriter/core/buildsettings.py | 9 +++++++++
novelwriter/tools/manuscript.py | 22 +++++++++++++++++++---
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index f8cc1c401..a187bd20f 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -502,6 +502,15 @@ def unpack(self, data: dict) -> None:
return
+ @classmethod
+ def duplicate(cls, source: BuildSettings) -> BuildSettings:
+ """Make a copy of another build."""
+ cls = BuildSettings()
+ cls.unpack(source.pack())
+ cls._uuid = str(uuid.uuid4())
+ cls._name = f"{source.name} 2"
+ return cls
+
class BuildCollection:
"""Core: Build Collection Class
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index dfcb4d3d0..cad132927 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -116,6 +116,11 @@ def __init__(self, parent: GuiMain) -> None:
self.tbDel.setStyleSheet(buttonStyle)
self.tbDel.clicked.connect(self._deleteSelectedBuild)
+ self.tbCopy = NIconToolButton(self, iSz, "copy")
+ self.tbCopy.setToolTip(self.tr("Duplicate Selected Build"))
+ self.tbCopy.setStyleSheet(buttonStyle)
+ self.tbCopy.clicked.connect(self._copySelectedBuild)
+
self.tbEdit = NIconToolButton(self, iSz, "edit")
self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
self.tbEdit.setStyleSheet(buttonStyle)
@@ -128,6 +133,7 @@ def __init__(self, parent: GuiMain) -> None:
self.listToolBox.addStretch(1)
self.listToolBox.addWidget(self.tbAdd)
self.listToolBox.addWidget(self.tbDel)
+ self.listToolBox.addWidget(self.tbCopy)
self.listToolBox.addWidget(self.tbEdit)
self.listToolBox.setSpacing(0)
@@ -291,6 +297,17 @@ def _editSelectedBuild(self) -> None:
self._openSettingsDialog(build)
return
+ @pyqtSlot()
+ def _copySelectedBuild(self) -> None:
+ """Copy the currently selected build settings entry."""
+ if build := self._getSelectedBuild():
+ new = BuildSettings.duplicate(build)
+ self._builds.setBuild(new)
+ self._updateBuildsList()
+ if item := self._buildMap.get(new.buildID):
+ item.setSelected(True)
+ return
+
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
"""Process change of build selection to update the details."""
@@ -460,9 +477,8 @@ def _updateBuildsList(self) -> None:
def _updateBuildItem(self, build: BuildSettings) -> None:
"""Update the entry of a specific build item."""
- bItem = self._buildMap.get(build.buildID, None)
- if isinstance(bItem, QListWidgetItem):
- bItem.setText(build.name)
+ if item := self._buildMap.get(build.buildID):
+ item.setText(build.name)
else: # Probably a new item
self._updateBuildsList()
return
From b088b79ee862019dffed9206ef7babdd4693e48d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 5 Nov 2024 18:32:02 +0100
Subject: [PATCH 3/3] Add test coverage
---
tests/test_core/test_core_buildsettings.py | 42 ++++++++++++++++++++++
tests/test_tools/test_tools_manuscript.py | 9 +++++
2 files changed, 51 insertions(+)
diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py
index 5d0048345..e41f5f4a0 100644
--- a/tests/test_core/test_core_buildsettings.py
+++ b/tests/test_core/test_core_buildsettings.py
@@ -465,3 +465,45 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
]
assert another.lastBuild == buildIDOne
assert another.defaultBuild == buildIDTwo
+
+
+@pytest.mark.core
+def testCoreBuildSettings_Duplicate(monkeypatch, mockGUI, fncPath: Path, mockRnd):
+ """Test duplicating builds."""
+ project = NWProject()
+ buildTestProject(project, fncPath)
+ buildsFile = project.storage.getMetaFile(nwFiles.BUILDS_FILE)
+ assert isinstance(buildsFile, Path)
+
+ # No initial builds in a fresh project
+ builds = BuildCollection(project)
+ assert len(builds) == 0
+ assert not buildsFile.exists()
+ assert builds.lastBuild == ""
+ assert builds.defaultBuild == ""
+
+ # Create a default build
+ buildOne = BuildSettings()
+ buildOne.setName("Test Build")
+ buildOne.setValue("headings.fmtScene", nwHeadFmt.TITLE)
+ buildOne.setValue("headings.fmtAltScene", nwHeadFmt.TITLE)
+ buildOne.setValue("headings.fmtSection", nwHeadFmt.TITLE)
+
+ # Copy it
+ buildTwo = BuildSettings().duplicate(buildOne)
+ assert buildTwo.name == "Test Build 2"
+
+ # Raw data
+ dataOne = buildOne.pack()
+ dataTwo = buildTwo.pack()
+
+ # Name and UUID should be different
+ assert dataOne["name"] != dataTwo["name"]
+ assert dataOne["uuid"] != dataTwo["uuid"]
+
+ # The rest should be equal
+ assert dataOne["path"] == dataTwo["path"]
+ assert dataOne["build"] == dataTwo["build"]
+ assert dataOne["format"] == dataTwo["format"]
+ assert dataOne["settings"] == dataTwo["settings"]
+ assert dataOne["content"] == dataTwo["content"]
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index 67f64c2a2..df5287510 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -138,6 +138,15 @@ def _testNewSettingsReady(new: BuildSettings):
assert build.name == "Test Build"
assert manus.buildList.count() == 1
+ # Copy the build
+ manus._buildMap[build.buildID].setSelected(True)
+ manus._copySelectedBuild()
+ assert manus.buildList.count() == 2
+
+ new = manus._getSelectedBuild()
+ assert new is not None
+ assert new.name == "Test Build 2"
+
# Close the dialog should also close the child dialogs
manus.btnClose.click()
if isinstance(bSettings, GuiBuildSettings):