#include "UI.hpp" #include "Model/Workflow/Nodes/DocumentNodes.hpp" #include "Model/Workflow/Nodes/NumericNodes.hpp" #include "Model/Workflow/Nodes/TextNodes.hpp" #include "Model/Workflow/Nodes/UserInputNodes.hpp" #include "Model/Workflow/Workflow.hpp" #include "UI/Localization.hpp" #include "Utils/Macros.hpp" #include #include #include #include #include namespace ImNodes = ax::NodeEditor; namespace { class WorkflowDatabase { public: using WorkflowNodeConstructor = std::unique_ptr (*)(); enum Category { NumericCategory, TextCategory, DocumentsCategory, UserInputCategory, SystemInputCategory, OutputCategory, }; struct Candidate { WorkflowNodeConstructor Constructor; std::string Name; Category Category; }; private: std::vector mCandidates; #define SUB_RANGE_ACCESS(Type, AccessorName, storage, begin, nextBegin) \ std::span AccessorName() \ { \ return { &storage[begin], (size_t)(nextBegin - begin) }; \ } int mTextOffset; int mDocumentOffset; int mUserInputOffset; int mSystemInputNodes; int mOutputOffset; public: SUB_RANGE_ACCESS(Candidate, GetNumericNodes, mCandidates, 0, mTextOffset); SUB_RANGE_ACCESS(Candidate, GetTextNodes, mCandidates, mTextOffset, mDocumentOffset); SUB_RANGE_ACCESS(Candidate, GetDocumentNodes, mCandidates, mDocumentOffset, mUserInputOffset); SUB_RANGE_ACCESS(Candidate, GetUserInputNodes, mCandidates, mUserInputOffset, mSystemInputNodes); SUB_RANGE_ACCESS(Candidate, GetSystemInputNodes, mCandidates, mSystemInputNodes, mOutputOffset); SUB_RANGE_ACCESS(Candidate, GetOutputNodes, mCandidates, mOutputOffset, mCandidates.size()); #undef SUB_RANGE_ACCESS public: void SetupCandidates() { // Numeric nodes offset start at 0 mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Addition); }, .Name = "Add", .Category = NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Subtraction); }, .Name = "Subtract", .Category = NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Multiplication); }, .Name = "Multiply", .Category = NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Division); }, .Name = "Divide", .Category = NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Evaluate expression", .Category = NumericCategory, }); mTextOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Fill template text", .Category = TextCategory, }); mDocumentOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Document template", .Category = Category::DocumentsCategory, }); /* Inputs */ mUserInputOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Input: form", .Category = Category::UserInputCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Input: database rows", .Category = Category::UserInputCategory, }); mSystemInputNodes = mCandidates.size(); /* Outputs */ mOutputOffset = mCandidates.size(); } }; class WorkflowUI { private: Workflow* mWorkflow; ImNodes::EditorContext* mContext; WorkflowDatabase mWorkflowDatabase; public: WorkflowUI() { mContext = ImNodes::CreateEditor(); mWorkflowDatabase.SetupCandidates(); } ~WorkflowUI() { ImNodes::DestroyEditor(mContext); } void Draw() { ImNodes::SetCurrentEditor(mContext); ImNodes::Begin(""); const char* tooltipMessage = nullptr; for (auto& node : mWorkflow->GetNodes()) { if (!node) continue; ImNodes::BeginNode(node->GetId()); node->Draw(); ImNodes::EndNode(); } for (auto& conn : mWorkflow->GetConnections()) { if (!conn.IsValid()) continue; auto srcId = mWorkflow->GetNodes()[conn.SourceNode]->GetOutputPinUniqueId(conn.SourcePin); auto dstId = mWorkflow->GetNodes()[conn.DestinationNode]->GetInputPinUniqueId(conn.DestinationPin); ImNodes::Link(conn.GetLinkId(), srcId, dstId); } if (ImNodes::BeginCreate()) { ImNodes::PinId src = 0, dst = 0; if (ImNodes::QueryNewLink(&src, &dst)) { if (!src || !dst) { goto createError; } auto [srcNode, srcPinId, srcIsOutput] = mWorkflow->DisassembleGlobalPinId((size_t)src); auto [dstNode, dstPinId, dstIsOutput] = mWorkflow->DisassembleGlobalPinId((size_t)dst); if (srcNode == dstNode) { ImNodes::RejectNewItem(); goto createError; } if (srcIsOutput == dstIsOutput) { ImNodes::RejectNewItem(); goto createError; } auto srcPin = srcNode->GetOutputPin(srcPinId); auto dstPin = dstNode->GetOutputPin(dstPinId); if (srcPin.MatchingType != dstPin.MatchingType) { ImNodes::RejectNewItem(); goto createError; } if (ImNodes::AcceptNewItem()) { mWorkflow->Connect(*srcNode, srcPinId, *dstNode, dstPinId); } } ImNodes::PinId newNodePin = 0; if (ImNodes::QueryNewNode(&newNodePin)) { auto [node, pinId, isOutput] = mWorkflow->DisassembleGlobalPinId((size_t)newNodePin); if ((isOutput && node->GetOutputPin(pinId).IsConnected()) || (!isOutput && node->GetInputPin(pinId).IsConnected())) { ImNodes::RejectNewItem(); goto createError; } if (ImNodes::AcceptNewItem()) { ImNodes::Suspend(); ImGui::BeginPopup("Create Node"); ImNodes::Resume(); } } } createError: ImNodes::EndCreate(); if (ImNodes::BeginDelete()) { ImNodes::LinkId deletedLinkId; if (ImNodes::QueryDeletedLink(&deletedLinkId)) { // Link id is the same as our connection id auto& conn = *mWorkflow->GetConnectionById((size_t)deletedLinkId); // TODO } } deleteError: ImNodes::EndDelete(); // Popups ImNodes::Suspend(); if (ImGui::BeginPopup("Node Context Menu")) { // TODO ImGui::EndPopup(); } if (ImGui::BeginPopup("Link Context Menu")) { // TODO ImGui::EndPopup(); } if (ImGui::BeginPopup("Create Node")) { auto DisplayCandidatesCategory = [&](const char* name, std::span candidates) { ImGui::TreeNode(name); for (auto& candidate : candidates) { if (ImGui::MenuItem(candidate.Name.c_str())) { // TODO } } ImGui::TreePop(); }; DisplayCandidatesCategory("Numeric nodes", mWorkflowDatabase.GetNumericNodes()); DisplayCandidatesCategory("Text nodes", mWorkflowDatabase.GetTextNodes()); DisplayCandidatesCategory("Document nodes", mWorkflowDatabase.GetDocumentNodes()); DisplayCandidatesCategory("User input nodes", mWorkflowDatabase.GetUserInputNodes()); DisplayCandidatesCategory("System input nodes", mWorkflowDatabase.GetSystemInputNodes()); DisplayCandidatesCategory("Output nodes", mWorkflowDatabase.GetOutputNodes()); ImGui::EndPopup(); } if (tooltipMessage) { ImGui::BeginTooltip(); ImGui::TextUnformatted(tooltipMessage); ImGui::EndTooltip(); } ImNodes::Resume(); ImNodes::End(); } }; } // namespace void UI::WorkflowsTab() { auto ls = LocaleStrings::Instance.get(); static std::unique_ptr openWorkflow; if (ImGui::Button(ls->Close.Get(), openWorkflow == nullptr)) { openWorkflow = nullptr; } ImGui::SameLine(); if (ImGui::Button(ls->OpenWorkflow.Get())) { ImGui::OpenPopup(ls->OpenWorkflowDialogTitle.Get()); } if (ImGui::BeginPopupModal(ls->OpenWorkflowDialogTitle.Get())) { // TODO ImGui::EndPopup(); } ImGui::SameLine(); if (ImGui::Button(ls->ManageWorkflows.Get())) { ImGui::OpenPopup(ls->ManageWorkflowsDialogTitle.Get()); } if (ImGui::BeginPopupModal(ls->ManageWorkflowsDialogTitle.Get())) { // TODO ImGui::EndPopup(); } if (openWorkflow) { openWorkflow->Draw(); } }