aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'app/source/Cplt/Model/Workflow/Workflow_Main.cpp')
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow_Main.cpp846
1 files changed, 846 insertions, 0 deletions
diff --git a/app/source/Cplt/Model/Workflow/Workflow_Main.cpp b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
new file mode 100644
index 0000000..0f35b32
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
@@ -0,0 +1,846 @@
+#include "Workflow.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/Archive.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <imgui.h>
+#include <imgui_node_editor.h>
+#include <imgui_stdlib.h>
+#include <tsl/robin_set.h>
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <fstream>
+#include <iostream>
+#include <queue>
+#include <utility>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+namespace ImNodes = ax::NodeEditor;
+
+WorkflowConnection::WorkflowConnection()
+ : Id{ 0 }
+ , SourceNode{ WorkflowNode::kInvalidId }
+ , SourcePin{ WorkflowNode::kInvalidPinId }
+ , DestinationNode{ WorkflowNode::kInvalidId }
+ , DestinationPin{ WorkflowNode::kInvalidPinId }
+{
+}
+
+bool WorkflowConnection::IsValid() const
+{
+ return Id != 0;
+}
+
+ImNodes::LinkId WorkflowConnection::GetLinkId() const
+{
+ // Our id is 0-based (represents an index directly)
+ // but imgui-node-editor uses the value 0 to represent a null id, so we need to offset by 1
+ return Id + 1;
+}
+
+void WorkflowConnection::DrawDebugInfo() const
+{
+ ImGui::Text("Source (node with output pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", SourceNode, SourcePin);
+ ImGui::Text("Destination (node with input pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", DestinationNode, DestinationPin);
+}
+
+void WorkflowConnection::ReadFrom(std::istream& stream)
+{
+ stream >> SourceNode >> SourcePin;
+ stream >> DestinationNode >> DestinationPin;
+}
+
+void WorkflowConnection::WriteTo(std::ostream& stream) const
+{
+ stream << SourceNode << SourcePin;
+ stream << DestinationNode << DestinationPin;
+}
+
+bool WorkflowNode::InputPin::IsConstantConnection() const
+{
+ return ConnectionToConst && IsConnected();
+}
+
+bool WorkflowNode::InputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+bool WorkflowNode::OutputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+WorkflowNode::WorkflowNode(Kind kind, bool locked)
+ : mKind{ kind }
+ , mDepth{ -1 }
+ , mLocked(locked)
+{
+}
+
+Vec2i WorkflowNode::GetPosition() const
+{
+ return mPosition;
+}
+
+void WorkflowNode::SetPosition(const Vec2i& position)
+{
+ mPosition = position;
+}
+
+uint32_t WorkflowNode::GetId() const
+{
+ return mId;
+}
+
+ImNodes::NodeId WorkflowNode::GetNodeId() const
+{
+ // See WorkflowConnection::GetLinkId for the rationale
+ return mId + 1;
+}
+
+WorkflowNode::Kind WorkflowNode::GetKind() const
+{
+ return mKind;
+}
+
+int WorkflowNode::GetDepth() const
+{
+ return mDepth;
+}
+
+bool WorkflowNode::IsLocked() const
+{
+ return mLocked;
+}
+
+WorkflowNode::Type WorkflowNode::GetType() const
+{
+ if (IsInputNode()) {
+ return InputType;
+ } else if (IsOutputNode()) {
+ return OutputType;
+ } else {
+ return TransformType;
+ }
+}
+
+bool WorkflowNode::IsInputNode() const
+{
+ return mInputs.size() == 0;
+}
+
+bool WorkflowNode::IsOutputNode() const
+{
+ return mOutputs.size() == 0;
+}
+
+void WorkflowNode::ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId)
+{
+ mWorkflow->Connect(*this, pinId, srcNode, srcPinId);
+}
+
+void WorkflowNode::DisconnectInput(uint32_t pinId)
+{
+ mWorkflow->DisconnectByDestination(*this, pinId);
+}
+
+void WorkflowNode::DrawInputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (input) %d", pinId);
+}
+
+const WorkflowNode::InputPin& WorkflowNode::GetInputPin(uint32_t pinId) const
+{
+ return mInputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetInputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, false);
+}
+
+void WorkflowNode::ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId)
+{
+ mWorkflow->Connect(dstNode, dstPinId, *this, pinId);
+}
+
+void WorkflowNode::DisconnectOutput(uint32_t pinId)
+{
+ mWorkflow->DisconnectBySource(*this, pinId);
+}
+
+void WorkflowNode::DrawOutputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (output) %d", pinId);
+}
+
+const WorkflowNode::OutputPin& WorkflowNode::GetOutputPin(uint32_t pinId) const
+{
+ return mOutputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetOutputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, true);
+}
+
+void WorkflowNode::Draw()
+{
+ for (uint32_t i = 0; i < mInputs.size(); ++i) {
+ auto& pin = mInputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetInputPinUniqueId(i), ImNodes::PinKind::Input);
+ // TODO
+ ImNodes::EndPin();
+ }
+ for (uint32_t i = 0; i < mOutputs.size(); ++i) {
+ auto& pin = mOutputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetOutputPinUniqueId(i), ImNodes::PinKind::Output);
+ // TODO
+ ImNodes::EndPin();
+ }
+}
+
+void WorkflowNode::DrawDebugInfo() const
+{
+ ImGui::Text("Node kind: %s", FormatKind(mKind));
+ ImGui::Text("Node type: %s", FormatType(GetType()));
+ ImGui::Text("Node ID: %u", mId);
+ ImGui::Text("Depth: %d", mDepth);
+ DrawExtraDebugInfo();
+}
+
+void WorkflowNode::ReadFrom(std::istream& stream)
+{
+ stream >> mId;
+ stream >> mPosition.x >> mPosition.y;
+}
+
+void WorkflowNode::WriteTo(std::ostream& stream)
+{
+ stream << mId;
+ stream << mPosition.x << mPosition.y;
+}
+
+WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mInputs.size());
+
+ mInputs.push_back(InputPin{});
+ for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapInputPin(i, i + 1);
+ }
+
+ return mInputs[atIdx];
+}
+
+void WorkflowNode::RemoveInputPin(int pin)
+{
+ DisconnectInput(pin);
+ for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mInputs.resize(mInputs.size() - 1);
+}
+
+void WorkflowNode::SwapInputPin(int a, int b)
+{
+ auto& pinA = mInputs[a];
+ auto& pinB = mInputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected() && !pinA.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.DestinationPin = b;
+ }
+ if (pinB.IsConnected() && !pinB.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.DestinationPin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mOutputs.size());
+
+ mOutputs.push_back(OutputPin{});
+ for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapOutputPin(i, i + 1);
+ }
+
+ return mOutputs[atIdx];
+}
+
+void WorkflowNode::RemoveOutputPin(int pin)
+{
+ DisconnectOutput(pin);
+ for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mOutputs.resize(mOutputs.size() - 1);
+}
+
+void WorkflowNode::SwapOutputPin(int a, int b)
+{
+ auto& pinA = mOutputs[a];
+ auto& pinB = mOutputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.SourcePin = b;
+ }
+ if (pinB.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.SourcePin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+void WorkflowNode::OnAttach(Workflow& workflow, uint32_t newId)
+{
+}
+
+void WorkflowNode::OnDetach()
+{
+}
+
+const std::vector<WorkflowConnection>& Workflow::GetConnections() const
+{
+ return mConnections;
+}
+
+std::vector<WorkflowConnection>& Workflow::GetConnections()
+{
+ return mConnections;
+}
+
+const std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() const
+{
+ return mNodes;
+}
+
+std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes()
+{
+ return mNodes;
+}
+
+const std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() const
+{
+ return mConstants;
+}
+
+std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants()
+{
+ return mConstants;
+}
+
+WorkflowConnection* Workflow::GetConnectionById(uint32_t id)
+{
+ return &mConnections[id];
+}
+
+WorkflowConnection* Workflow::GetConnectionByLinkId(ImNodes::LinkId id)
+{
+ return &mConnections[(uint32_t)(size_t)id - 1];
+}
+
+WorkflowNode* Workflow::GetNodeById(uint32_t id)
+{
+ return mNodes[id].get();
+}
+
+WorkflowNode* Workflow::GetNodeByNodeId(ImNodes::NodeId id)
+{
+ return mNodes[(uint32_t)(size_t)id - 1].get();
+}
+
+BaseValue* Workflow::GetConstantById(uint32_t id)
+{
+ return mConstants[id].get();
+}
+
+Workflow::GlobalPinId Workflow::DisassembleGlobalPinId(ImNodes::PinId pinId)
+{
+ // imgui-node-editor requires all pins to have a global, unique id
+ // but in our model the pin are typed (input vs output) and associated with a node: there is no built-in global id
+ // Therefore we encode one ourselves
+
+ // Global pin id format
+ // nnnnnnnn nnnnnnnn nnnnnnnn nnnnnnnn Tppppppp ppppppppp pppppppp pppppppp
+ // <------- (32 bits) node id -------> ^<------ (31 bits) pin id -------->
+ // | (1 bit) input (false) vs output (true)
+
+ // 1 is added to pin id to prevent the 0th node's 0th input pin resulting in a 0 global pin id
+ // (this is problematic because imgui-node-editor use 0 to represent null)
+
+ auto id = static_cast<uint64_t>(pinId);
+ GlobalPinId result;
+
+ result.Node = mNodes[id >> 32].get();
+ result.PinId = (uint32_t)(id & 0x000000001FFFFFFF) - 1;
+ result.IsOutput = id >> 31;
+
+ return result;
+}
+
+ImNodes::PinId Workflow::FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const
+{
+ // See this->DisassembleGlobalPinId for format details and rationale
+
+ uint64_t id = 0;
+ id |= ((uint64_t)node.GetId() << 32);
+ id |= (isOutput << 31);
+ id |= ((pinId + 1) & 0x1FFFFFFF);
+
+ return id;
+}
+
+const std::vector<std::vector<uint32_t>>& Workflow::GetDepthGroups() const
+{
+ return mDepthGroups;
+}
+
+bool Workflow::DoesDepthNeedsUpdate() const
+{
+ return mDepthsDirty;
+}
+
+void Workflow::AddNode(std::unique_ptr<WorkflowNode> step)
+{
+ auto [storage, id] = AllocWorkflowStep();
+ storage = std::move(step);
+ storage->OnAttach(*this, id);
+ storage->mWorkflow = this;
+ storage->mId = id;
+}
+
+void Workflow::RemoveNode(uint32_t id)
+{
+ auto& step = mNodes[id];
+ if (step == nullptr) return;
+
+ step->OnDetach();
+ step->mWorkflow = nullptr;
+ step->mId = WorkflowNode::kInvalidId;
+}
+
+void Workflow::RemoveConnection(uint32_t id)
+{
+ auto& conn = mConnections[id];
+ if (!conn.IsValid()) return;
+
+ mNodes[conn.SourceNode]->mInputs[conn.SourcePin].Connection = WorkflowNode::kInvalidId;
+ mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin].Connection = WorkflowNode::kInvalidId;
+
+ conn = {};
+ mDepthsDirty = true;
+}
+
+bool Workflow::Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& src = sourceNode.mOutputs[sourcePin];
+ auto& dst = destinationNode.mInputs[destinationPin];
+
+ // TODO report error to user?
+ if (src.GetMatchingType() != dst.GetMatchingType()) {
+ return false;
+ }
+
+ if (src.IsConnected()) {
+ DisconnectBySource(sourceNode, sourcePin);
+ }
+
+ auto [conn, id] = AllocWorkflowConnection();
+ conn.SourceNode = sourceNode.GetId();
+ conn.SourcePin = sourcePin;
+ conn.DestinationNode = destinationNode.GetId();
+ conn.DestinationPin = destinationPin;
+
+ src.Connection = id;
+ dst.Connection = id;
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin)
+{
+ auto& sn = sourceNode.mOutputs[sourcePin];
+ if (!sn.IsConnected()) return false;
+
+ auto& conn = mConnections[sn.Connection];
+ auto& dn = mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& dn = destinationNode.mOutputs[destinationPin];
+ if (!dn.IsConnected()) return false;
+
+ auto& conn = mConnections[dn.Connection];
+ auto& sn = mNodes[conn.SourceNode]->mInputs[conn.SourcePin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+Workflow::GraphUpdateResult Workflow::UpdateGraph(GraphUpdateDetails* details)
+{
+ if (!mDepthsDirty) {
+ return GUR_NoWorkToDo;
+ }
+
+ // Terminology:
+ // - Dependency = nodes its input pins are connected to
+ // - Dependents = nodes its output pins are connected to
+
+ struct WorkingNode
+ {
+ // The max depth out of all dependency nodes, maintained during the traversal and committed as the actual depth
+ // when all dependencies of this node has been resolved. Add 1 to get the depth that will be assigned to the node.
+ int MaximumDepth = 0;
+ int FulfilledInputCount = 0;
+ };
+
+ std::vector<WorkingNode> workingNodes;
+ std::queue<uint32_t> q;
+
+ // Check if all dependencies of this node is satisfied
+ auto CheckNodeDependencies = [&](WorkflowNode& node) -> bool {
+ for (auto& pin : node.mInputs) {
+ if (!pin.IsConnected()) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ workingNodes.reserve(mNodes.size());
+ {
+ std::vector<uint32_t> unsatisfiedNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& node = mNodes[i];
+ workingNodes.push_back(WorkingNode{});
+
+ if (!node) continue;
+
+ if (!CheckNodeDependencies(*node)) {
+ unsatisfiedNodes.push_back(i);
+ }
+
+ node->mDepth = -1;
+
+ // Start traversing with the input nodes
+ if (node->GetType() == WorkflowNode::InputType) {
+ q.push(i);
+ }
+ }
+
+ if (!unsatisfiedNodes.empty()) {
+ if (details) {
+ details->emplace<decltype(unsatisfiedNodes)>(std::move(unsatisfiedNodes));
+ }
+ return GUR_UnsatisfiedDependencies;
+ }
+ }
+
+ auto ProcessNode = [&](WorkflowNode& node) -> void {
+ for (auto& pin : node.mOutputs) {
+ if (!pin.IsConnected()) continue;
+ auto& conn = mConnections[pin.Connection];
+
+ auto& wn = workingNodes[conn.DestinationNode];
+ auto& n = *mNodes[conn.DestinationPin].get();
+
+ wn.FulfilledInputCount++;
+ wn.MaximumDepth = std::max(node.mDepth, wn.MaximumDepth);
+
+ // Node's dependency is fulfilled, we can process its dependents next
+ // We use >= here because for a many-to-one pin, the dependency is an "or" relation ship, i.e. any of the nodes firing before this will fulfill the requirement
+ if (n.mInputs.size() >= wn.FulfilledInputCount) {
+ n.mDepth = wn.MaximumDepth + 1;
+ }
+ }
+ };
+
+ int processedNodes = 0;
+ while (!q.empty()) {
+ auto& wn = workingNodes[q.front()];
+ auto& n = *mNodes[q.front()];
+ q.pop();
+ processedNodes++;
+
+ ProcessNode(n);
+ }
+
+ if (processedNodes < mNodes.size()) {
+ // There is unreachable nodes, collect them and report to the caller
+
+ std::vector<uint32_t> unreachableNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& wn = workingNodes[i];
+ auto& n = *mNodes[i];
+
+ // This is a reachable node
+ if (n.mDepth != -1) continue;
+
+ unreachableNodes.push_back(i);
+ }
+
+ if (details) {
+ details->emplace<decltype(unreachableNodes)>(std::move(unreachableNodes));
+ }
+ return GUR_UnreachableNodes;
+ }
+
+ return GUR_Success;
+}
+
+class Workflow::Private
+{
+public:
+ template <class TSelf, class TProxy>
+ static void OperateStream(TSelf& self, TProxy& proxy)
+ {
+ // TODO
+ }
+};
+
+void Workflow::ReadFromDataStream(InputDataStream& stream)
+{
+ Private::OperateStream(*this, stream);
+}
+
+void Workflow::WriteToDataStream(OutputDataStream& stream) const
+{
+ Private::OperateStream(*this, stream);
+}
+
+std::pair<WorkflowConnection&, uint32_t> Workflow::AllocWorkflowConnection()
+{
+ for (size_t idx = 0; idx < mConnections.size(); ++idx) {
+ auto& elm = mConnections[idx];
+ if (!elm.IsValid()) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mConnections.size();
+ auto& conn = mConnections.emplace_back(WorkflowConnection{});
+ conn.Id = id;
+
+ return { conn, id };
+}
+
+std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> Workflow::AllocWorkflowStep()
+{
+ for (size_t idx = 0; idx < mNodes.size(); ++idx) {
+ auto& elm = mNodes[idx];
+ if (elm == nullptr) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mNodes.size();
+ auto& node = mNodes.emplace_back(std::unique_ptr<WorkflowNode>());
+
+ return { node, id };
+}
+
+void WorkflowAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const
+{
+ auto dir = GetConnectedProject().GetWorkflowsDirectory();
+ DiscoverFilesByExtension(callback, dir, ".cplt-workflow"sv);
+}
+
+std::string WorkflowAssetList::RetrieveNameFromFile(const fs::path& file) const
+{
+ auto res = DataArchive::LoadFile(file);
+ if (!res) return "";
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ return assetInfo.Name;
+}
+
+uuids::uuid WorkflowAssetList::RetrieveUuidFromFile(const fs::path& file) const
+{
+ return uuids::uuid::from_string(file.stem().string());
+}
+
+fs::path WorkflowAssetList::RetrievePathFromAsset(const SavedAsset& asset) const
+{
+ auto fileName = uuids::to_string(asset.Uuid);
+ return GetConnectedProject().GetWorkflowPath(fileName);
+}
+
+bool WorkflowAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+ auto res = DataArchive::SaveFile(path);
+ if (!res) return false;
+ auto& stream = res.value();
+
+ stream.WriteObject(assetInfo);
+ // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Workflow
+ if (auto workflow = static_cast<const Workflow*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
+ stream.WriteObject(*workflow);
+ }
+
+ return true;
+}
+
+static std::unique_ptr<Workflow> LoadWorkflowFromFile(const fs::path& path)
+{
+ auto res = DataArchive::LoadFile(path);
+ if (!res) return nullptr;
+ auto& stream = res.value();
+
+ // TODO this is currently unused
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ auto workflow = std::make_unique<Workflow>();
+ stream.ReadObject(*workflow);
+
+ return workflow;
+}
+
+Workflow* WorkflowAssetList::LoadInstance(const SavedAsset& assetInfo) const
+{
+ return ::LoadWorkflowFromFile(RetrievePathFromAsset(assetInfo)).release();
+}
+
+Workflow* WorkflowAssetList::CreateInstance(const SavedAsset& assetInfo) const
+{
+ return new Workflow();
+}
+
+bool WorkflowAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+
+ auto workflow = ::LoadWorkflowFromFile(path);
+ if (!workflow) return false;
+
+ SaveInstance(assetInfo, workflow.get());
+
+ return true;
+}
+
+void WorkflowAssetList::DisplayAssetCreator(ListState& state)
+{
+ auto ValidateNewName = [&]() -> void {
+ if (mACNewName.empty()) {
+ mACNewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (FindByName(mACNewName)) {
+ mACNewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ mACNewNameError = NameSelectionError::None;
+ };
+
+ auto ShowNewNameErrors = [&]() -> void {
+ switch (mACNewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ };
+
+ auto IsInputValid = [&]() -> bool {
+ return mACNewNameError == NameSelectionError::None;
+ };
+
+ auto ResetState = [&]() -> void {
+ mACNewName.clear();
+ ValidateNewName();
+ };
+
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) {
+ ValidateNewName();
+ }
+
+ ShowNewNameErrors();
+
+ if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) {
+ ImGui::CloseCurrentPopup();
+
+ Create(SavedAsset{
+ .Name = mACNewName,
+ });
+ ResetState();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void WorkflowAssetList::DisplayDetailsTable(ListState& state) const
+{
+ ImGui::BeginTable("AssetDetailsTable", 1, ImGuiTableFlags_Borders);
+
+ ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME));
+ ImGui::TableHeadersRow();
+
+ for (auto& asset : this->GetAssets()) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) {
+ state.SelectedAsset = &asset;
+ }
+ }
+
+ ImGui::EndTable();
+}