#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 { enum class WorkflowCategory { NumericCategory, TextCategory, DocumentsCategory, UserInputCategory, SystemInputCategory, OutputCategory, }; class WorkflowDatabase { public: using WorkflowNodeConstructor = std::unique_ptr (*)(); struct Candidate { WorkflowNodeConstructor Constructor; std::string Name; WorkflowCategory 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 = WorkflowCategory::NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Subtraction); }, .Name = "Subtract", .Category = WorkflowCategory::NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Multiplication); }, .Name = "Multiply", .Category = WorkflowCategory::NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(NumericOperationNode::Division); }, .Name = "Divide", .Category = WorkflowCategory::NumericCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Evaluate expression", .Category = WorkflowCategory::NumericCategory, }); mTextOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Fill template text", .Category = WorkflowCategory::TextCategory, }); mDocumentOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Document template", .Category = WorkflowCategory::DocumentsCategory, }); /* Inputs */ mUserInputOffset = mCandidates.size(); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Input: form", .Category = WorkflowCategory::UserInputCategory, }); mCandidates.push_back(Candidate{ .Constructor = []() -> std::unique_ptr { return std::make_unique(); }, .Name = "Input: database rows", .Category = WorkflowCategory::UserInputCategory, }); mSystemInputNodes = mCandidates.size(); /* Outputs */ mOutputOffset = mCandidates.size(); } }; class WorkflowUI { private: Workflow* mWorkflow; ImNodes::EditorContext* mContext; WorkflowDatabase mWorkflowDatabase; ImNodes::NodeId mContextMenuNodeId = 0; ImNodes::PinId mContextMenuPinId = 0; ImNodes::LinkId mContextMenuLinkId = 0; public: WorkflowUI() { mContext = ImNodes::CreateEditor(); mWorkflowDatabase.SetupCandidates(); } ~WorkflowUI() { ImNodes::DestroyEditor(mContext); } void Draw() { auto ls = LocaleStrings::Instance.get(); ImNodes::SetCurrentEditor(mContext); ImNodes::Begin(""); // Defer creation of tooltip because within the node editor, cursor positioning is going to be off 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(src); auto [dstNode, dstPinId, dstIsOutput] = mWorkflow->DisassembleGlobalPinId(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(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)) { auto& conn = *mWorkflow->GetConnectionByLinkId(deletedLinkId); mWorkflow->RemoveConnection(conn.Id); } ImNodes::NodeId deletedNodeId; if (ImNodes::QueryDeletedNode(&deletedNodeId)) { auto node = mWorkflow->GetNodeByNodeId(deletedNodeId); if (!node) { ImNodes::RejectDeletedItem(); goto deleteError; } if (node->IsLocked()) { ImNodes::RejectDeletedItem(); goto deleteError; } } } deleteError: ImNodes::EndDelete(); // Popups ImNodes::Suspend(); if (ImNodes::ShowNodeContextMenu(&mContextMenuNodeId)) { ImGui::OpenPopup("Node Context Menu"); } else if (ImNodes::ShowPinContextMenu(&mContextMenuPinId)) { ImGui::OpenPopup("Pin Context Menu"); } else if (ImNodes::ShowLinkContextMenu(&mContextMenuLinkId)) { ImGui::OpenPopup("Link Context Menu"); } if (ImGui::BeginPopup("Node Context Menu")) { auto& node = *mWorkflow->GetNodeByNodeId(mContextMenuNodeId); node.DrawDebugInfo(); if (ImGui::MenuItem(ls->Delete.Get())) { ImNodes::DeleteNode(mContextMenuNodeId); } ImGui::EndPopup(); } if (ImGui::BeginPopup("Pin Context Menu")) { auto [node, pinId, isOutput] = mWorkflow->DisassembleGlobalPinId(mContextMenuPinId); if (isOutput) { node->DrawOutputPinDebugInfo(pinId); } else { node->DrawInputPinDebugInfo(pinId); } if (ImGui::MenuItem(ls->Disconnect.Get())) { if (isOutput) { auto& pin = node->GetOutputPin(pinId); if (pin.IsConnected()) { auto linkId = mWorkflow->GetConnectionById(pin.Connection)->GetLinkId(); ImNodes::DeleteLink(linkId); } } else { auto& pin = node->GetInputPin(pinId); if (pin.IsConstantConnection()) { // TODO } else if (pin.IsConnected()) { auto linkId = mWorkflow->GetConnectionById(pin.Connection)->GetLinkId(); ImNodes::DeleteLink(linkId); } } } ImGui::EndPopup(); } if (ImGui::BeginPopup("Link Context Menu")) { auto& conn = *mWorkflow->GetConnectionByLinkId(mContextMenuLinkId); conn.DrawDebugInfo(); if (ImGui::MenuItem(ls->Delete.Get())) { ImNodes::DeleteLink(mContextMenuLinkId); } ImGui::EndPopup(); } if (ImGui::BeginPopup("Create Node")) { auto DisplayCandidatesCategory = [&](const char* name, std::span candidates) { if (ImGui::BeginMenu(name)) { for (auto& candidate : candidates) { if (ImGui::MenuItem(candidate.Name.c_str())) { // Create node auto uptr = candidate.Constructor(); mWorkflow->AddNode(std::move(uptr)); } } ImGui::EndMenu(); } }; 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(); } }