Skip to content

Commit

Permalink
Add build copy feature (#2084)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Nov 5, 2024
2 parents 04036ef + b088b79 commit ae26138
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 7 deletions.
1 change: 1 addition & 0 deletions novelwriter/assets/icons/typicons_dark/icons.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions novelwriter/assets/icons/typicons_dark/mixed_copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions novelwriter/assets/icons/typicons_light/icons.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions novelwriter/assets/icons/typicons_light/mixed_copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions novelwriter/core/buildsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions novelwriter/gui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 19 additions & 3 deletions novelwriter/tools/manuscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions tests/test_core/test_core_buildsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
9 changes: 9 additions & 0 deletions tests/test_tools/test_tools_manuscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit ae26138

Please sign in to comment.