diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 0698c2e0..84dd7ff2 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -156,6 +156,7 @@ class BuildSettings: self._uuid = str(uuid.uuid4()) self._path = Path.home() self._build = "" + self._order = 0 self._format = nwBuildFmt.ODT self._skipRoot = set() self._excluded = set() @@ -164,20 +165,32 @@ class BuildSettings: self._changed = False return + @classmethod + def fromDict(cls, data: dict) -> BuildSettings: + """Create a build settings object from a dict.""" + cls = BuildSettings() + cls.unpack(data) + return cls + ## # Properties ## @property def name(self) -> str: - """The build name.""" + """Return the build name.""" return self._name @property def buildID(self) -> str: - """The build ID as a UUID.""" + """Return the build ID as a UUID.""" return self._uuid + @property + def order(self) -> int: + """Return the build order.""" + return self._order + @property def lastPath(self) -> Path: """The last used build path.""" @@ -251,6 +264,12 @@ class BuildSettings: self._uuid = value return + def setOrder(self, value: int) -> None: + """Set the build order.""" + if isinstance(value, int): + self._order = value + return + def setLastPath(self, path: Path | str | None) -> None: """Set the last used build path.""" if isinstance(path, str): @@ -398,6 +417,7 @@ class BuildSettings: "uuid": self._uuid, "path": str(self._path), "build": self._build, + "order": self._order, "format": self._format.name, "settings": self._settings.copy(), "content": { @@ -409,14 +429,15 @@ class BuildSettings: def unpack(self, data: dict) -> None: """Unpack a dictionary and populate the class.""" + content = data.get("content", {}) settings = data.get("settings", {}) - content = data.get("content", {}) included = content.get("included", []) excluded = content.get("excluded", []) skipRoot = content.get("skipRoot", []) self.setName(data.get("name", "")) self.setBuildID(data.get("uuid", "")) + self.setOrder(data.get("order", 0)) self.setLastPath(data.get("path", None)) self.setLastBuildName(data.get("build", "")) @@ -453,9 +474,9 @@ class BuildCollection: def __init__(self, project: NWProject) -> None: self._project = project - self._builds = {} self._lastBuild = "" self._defaultBuild = "" + self._builds: dict[str, BuildSettings] = {} self._loadCollection() return @@ -483,21 +504,19 @@ class BuildCollection: def getBuild(self, buildID: str) -> BuildSettings | None: """Get a specific build settings object.""" - if buildID not in self._builds: - return None - build = BuildSettings() - build.unpack(self._builds[buildID]) - return build + return self._builds.get(buildID, None) ## # Setters ## - def setLastBuild(self, buildID: str) -> None: + def setBuildsState(self, lastBuild: str, order: list[str]) -> None: """Set the last active build id.""" - if buildID != self._lastBuild: - self._lastBuild = buildID - self._saveCollection() + for i, key in enumerate(order): + if build := self._builds.get(key): + build.setOrder(i) + self._lastBuild = lastBuild + self._saveCollection() return def setDefaultBuild(self, buildID: str) -> None: @@ -510,8 +529,7 @@ class BuildCollection: def setBuild(self, build: BuildSettings) -> None: """Set build settings data in the collection.""" if isinstance(build, BuildSettings): - buildID = build.buildID - self._builds[buildID] = build.pack() + self._builds[build.buildID] = build self._saveCollection() return @@ -520,15 +538,15 @@ class BuildCollection: ## def removeBuild(self, buildID: str) -> None: - """Remove the a build from the collection.""" + """Remove a build from the collection.""" self._builds.pop(buildID, None) self._saveCollection() return def builds(self) -> Iterable[tuple[str, str]]: """Iterate over all available builds.""" - for buildID in self._builds: - yield buildID, self._builds[buildID].get("name", "") + for buildID, build in sorted(self._builds.items(), key=lambda x: x[1].order): + yield buildID, build.name return ## @@ -567,7 +585,7 @@ class BuildCollection: elif key == "defaultBuild": self._defaultBuild = str(entry) elif isinstance(entry, dict): - self._builds[key] = entry + self._builds[key] = BuildSettings.fromDict(entry) return True @@ -579,11 +597,11 @@ class BuildCollection: logger.debug("Saving builds file") try: - data = { + data: dict[str, str | dict] = { "lastBuild": self._lastBuild, "defaultBuild": self._defaultBuild, } - data.update(self._builds) + data.update({k: b.pack() for k, b in self._builds.items()}) with open(buildsFile, mode="w+", encoding="utf-8") as outFile: outFile.write(jsonEncode({"novelWriter.builds": data}, nmax=4)) except Exception: diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 3a8e2f91..34e7f2cf 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -144,6 +144,8 @@ class GuiManuscript(QDialog): self.buildList.setIconSize(QSize(iPx, iPx)) self.buildList.doubleClicked.connect(self._editSelectedBuild) self.buildList.currentItemChanged.connect(self._updateBuildDetails) + self.buildList.setSelectionMode(QAbstractItemView.SingleSelection) + self.buildList.setDragDropMode(QAbstractItemView.InternalMove) self.buildDetails = _DetailsWidget(self) self.buildDetails.setColumnWidth( @@ -417,9 +419,15 @@ class GuiManuscript(QDialog): """Save the user GUI settings.""" logger.debug("Saving GuiManuscript settings") + buildOrder = [] + for i in range(self.buildList.count()): + if item := self.buildList.item(i): + buildOrder.append(item.data(self.D_KEY)) + current = self.buildList.currentItem() - if isinstance(current, QListWidgetItem): - self._builds.setLastBuild(current.data(self.D_KEY)) + lastBuild = current.data(self.D_KEY) if isinstance(current, QListWidgetItem) else "" + + self._builds.setBuildsState(lastBuild, buildOrder) winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py index ca75cc40..eb4affdf 100644 --- a/tests/test_core/test_core_buildsettings.py +++ b/tests/test_core/test_core_buildsettings.py @@ -435,7 +435,7 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn (buildIDTwo, "Build Two"), (buildIDOne, "Build One"), ] - builds.setLastBuild(buildIDOne) + builds.setBuildsState(buildIDOne, [buildIDTwo, buildIDOne]) builds.setDefaultBuild(buildIDTwo) # Check errors: No valid path