diff options
author | rtk0c <[email protected]> | 2022-06-30 21:38:53 -0700 |
---|---|---|
committer | rtk0c <[email protected]> | 2022-06-30 21:38:53 -0700 |
commit | 7fe47a9d5b1727a61dc724523b530762f6d6ba19 (patch) | |
tree | e95be6e66db504ed06d00b72c579565bab873277 /app/source/Cplt/UI | |
parent | 2cf952088d375ac8b2f45b144462af0953436cff (diff) |
Restructure project
Diffstat (limited to 'app/source/Cplt/UI')
-rw-r--r-- | app/source/Cplt/UI/UI.hpp | 48 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_DatabaseView.cpp | 668 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_Items.cpp | 252 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_MainWindow.cpp | 237 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_Settings.cpp | 8 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_Templates.cpp | 977 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_Utils.cpp | 315 | ||||
-rw-r--r-- | app/source/Cplt/UI/UI_Workflows.cpp | 293 | ||||
-rw-r--r-- | app/source/Cplt/UI/fwd.hpp | 6 |
9 files changed, 2804 insertions, 0 deletions
diff --git a/app/source/Cplt/UI/UI.hpp b/app/source/Cplt/UI/UI.hpp new file mode 100644 index 0000000..0a80b4c --- /dev/null +++ b/app/source/Cplt/UI/UI.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include <imgui.h> + +namespace ImGui { + +void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond = ImGuiCond_None); +void SetNextWindowCentered(ImGuiCond cond = ImGuiCond_None); + +void PushDisabled(); +void PopDisabled(); + +bool Button(const char* label, bool disabled); +bool Button(const char* label, const ImVec2& sizeArg, bool disabled); + +void ErrorIcon(); +void ErrorMessage(const char* fmt, ...); +void WarningIcon(); +void WarningMessage(const char* fmt, ...); + +enum class IconType +{ + Flow, + Circle, + Square, + Grid, + RoundSquare, + Diamond, +}; + +void DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor); +void Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color = ImVec4(1, 1, 1, 1), const ImVec4& innerColor = ImVec4(0, 0, 0, 0)); + +bool Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize = -1.0f); + +} // namespace ImGui + +namespace UI { + +void MainWindow(); + +void SettingsTab(); +void DatabaseViewTab(); +void ItemsTab(); +void WorkflowsTab(); +void TemplatesTab(); + +} // namespace UI diff --git a/app/source/Cplt/UI/UI_DatabaseView.cpp b/app/source/Cplt/UI/UI_DatabaseView.cpp new file mode 100644 index 0000000..1e58eb0 --- /dev/null +++ b/app/source/Cplt/UI/UI_DatabaseView.cpp @@ -0,0 +1,668 @@ +#include "UI.hpp" + +#include <Cplt/Model/Filter.hpp> +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/ScopeGuard.hpp> +#include <Cplt/Utils/Time.hpp> + +#include <IconsFontAwesome.h> +#include <SQLiteCpp/Statement.h> +#include <imgui.h> +#include <tsl/robin_map.h> +#include <cstdint> +#include <iostream> +#include <memory> +#include <vector> + +namespace CPLT_UNITY_ID { + +// TODO move to Settings +constexpr int kMaxEntriesPerPage = 32; +constexpr int kSummaryItemCount = 3; +constexpr int kSummaryMaxLength = 25; + +std::pair<int, int> SplitEntryIndex(int entryIdx) +{ + int page = entryIdx / kMaxEntriesPerPage; + int row = entryIdx % kMaxEntriesPerPage; + return { page, row }; +} + +enum class DeliveryDirection +{ + FactoryToWarehouse, + WarehouseToCustomer, +}; + +struct Item +{ + int ItemId; + int Count; +}; + +struct DeliveryEntry +{ + std::vector<Item> Items; + std::string ItemsSummary; + std::string ShipmentTime; + std::string ArriveTime; + DeliveryDirection Direction; + + const char* StringifyDirection() const + { + switch (Direction) { + case DeliveryDirection::FactoryToWarehouse: return "Factory to warehouse"; + case DeliveryDirection::WarehouseToCustomer: return "Warehouse to customer"; + } + } +}; + +struct SaleEntry +{ + static constexpr auto kType = DeliveryDirection::WarehouseToCustomer; + + std::vector<DeliveryEntry> AssociatedDeliveries; + std::vector<Item> Items; + std::string ItemsSummary; + std::string Customer; + std::string Deadline; + std::string DeliveryTime; + int Id; + bool DeliveriesCached = false; +}; + +struct PurchaseEntry +{ + static constexpr auto kType = DeliveryDirection::FactoryToWarehouse; + + std::vector<DeliveryEntry> AssociatedDeliveries; + std::vector<Item> Items; + std::string ItemsSummary; + std::string Factory; + std::string OrderTime; + std::string DeliveryTime; + int Id; + bool DeliveriesCached; +}; + +template <class T> +class GenericTableView +{ +public: + // clang-format off + static constexpr bool kHasItems = requires(T t) + { + t.Items; + t.ItemsSummary; + }; + static constexpr bool kHasCustomer = requires(T t) { t.Customer; }; + static constexpr bool kHasDeadline = requires(T t) { t.Deadline; }; + static constexpr bool kHasFactory = requires(T t) { t.Factory; }; + static constexpr bool kHasOrderTime = requires(T t) { t.OrderTime; }; + static constexpr bool kHasCompletionTime = requires(T t) { t.DeliveryTime; }; + static constexpr int kColumnCount = kHasItems + kHasCustomer + kHasDeadline + kHasFactory + kHasOrderTime + kHasCompletionTime; + // clang-format on + + using Page = std::vector<T>; + + struct QueryStatements + { + SQLite::Statement* GetRowCount; + SQLite::Statement* GetRows; + SQLite::Statement* GetItems; + SQLite::Statement* FilterRows; + } Statements; + +protected: + // Translation entries for implementer to fill out + const char* mEditDialogTitle; + + Project* mProject; + Page* mCurrentPage = nullptr; + + /// Current active filter object, or \c nullptr. + std::unique_ptr<TableRowsFilter> mActiveFilter; + + tsl::robin_map<int, Page> mPages; + + /// A vector of entry indices (in \c mEntries) that are visible under the current filter. + std::vector<int> mActiveEntries; + + /// Number of rows in the table. + int mRowCount; + /// Last possible page for the current set table and filter (inclusive). + int mLastPage; + + /// The current page the user is on. + int mCurrentPageNumber; + + /// Row index of the select entry + int mSelectRow; + +public: + /// Calculate the first visible row's entry index. + int GetFirstVisibleRowIdx() const + { + return mCurrentPageNumber * kMaxEntriesPerPage; + } + + Project* GetProject() const + { + return mProject; + } + + void OnProjectChanged(Project* newProject) + { + mProject = newProject; + + auto& stmt = *Statements.GetRowCount; + if (stmt.executeStep()) { + mRowCount = stmt.getColumn(0).getInt(); + } else { + std::cerr << "Failed to fetch row count from SQLite.\n"; + mRowCount = 0; + } + + mActiveFilter = nullptr; + mActiveEntries.clear(); + + mPages.clear(); + mCurrentPage = nullptr; + UpdateLastPage(); + SetPage(0); + + mSelectRow = -1; + } + + TableRowsFilter* GetFilter() const + { + return mActiveFilter.get(); + } + + void OnFilterChanged() + { + auto& stmt = *Statements.FilterRows; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + // TODO lazy loading when too many results + mActiveEntries.clear(); + int columnIdx = stmt.getColumnIndex("Id"); + while (stmt.executeStep()) { + mActiveEntries.push_back(stmt.getColumn(columnIdx).getInt()); + } + + UpdateLastPage(); + SetPage(0); + + mSelectRow = -1; + } + + void OnFilterChanged(std::unique_ptr<TableRowsFilter> filter) + { + mActiveFilter = std::move(filter); + OnFilterChanged(); + } + + void Display() + { + bool dummy = true; + + if (ImGui::Button(ICON_FA_ARROW_LEFT, mCurrentPageNumber == 0)) { + SetPage(mCurrentPageNumber - 1); + } + + ImGui::SameLine(); + // +1 to convert from 0-based indices to 1-based, for human legibility + ImGui::Text("%d/%d", mCurrentPageNumber + 1, mLastPage + 1); + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_RIGHT, mCurrentPageNumber == mLastPage)) { + SetPage(mCurrentPageNumber + 1); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + // TODO + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT " " I18N_TEXT("Edit", L10N_EDIT), mSelectRow == -1)) { + ImGui::OpenPopup(mEditDialogTitle); + } + if (ImGui::BeginPopupModal(mEditDialogTitle, &dummy, ImGuiWindowFlags_AlwaysAutoResize)) { + auto& entry = (*mCurrentPage)[mSelectRow]; + int entryIdx = GetFirstVisibleRowIdx() + mSelectRow; + EditEntry(entry, entryIdx, mSelectRow); + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), mSelectRow == -1)) { + // TODO + } + + ImGui::Columns(2); + { + DisplayMainTable(); + ImGui::NextColumn(); + + if (mSelectRow == -1) { + ImGui::TextWrapped("%s", I18N_TEXT("Select an entry to show associated deliveries", L10N_DATABASE_MESSAGE_NO_ORDER_SELECTED)); + } else { + DisplayDeliveriesTable(); + } + ImGui::NextColumn(); + } + ImGui::Columns(1); + } + + void SetPage(int page) + { + mCurrentPageNumber = page; + mCurrentPage = &LoadAndGetPage(page); + mSelectRow = -1; + } + +private: + static int CalcPageForRowId(int64_t entryIdx) + { + return entryIdx / kMaxEntriesPerPage; + } + + /// Calculate range [begin, end) of index for the list of entries that are currently visible that the path-th page would show. + /// i.e. when there is a filter, look into \c mActiveEntryIndices; when there is no filter, use directly. + static std::pair<int64_t, int64_t> CalcRangeForPage(int page) + { + int begin = page * kMaxEntriesPerPage; + return { begin, begin + kMaxEntriesPerPage }; + } + + void DisplayMainTable() + { + if (ImGui::BeginTable("DataTable", kColumnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { + + if constexpr (kHasCustomer) ImGui::TableSetupColumn(I18N_TEXT("Customer", L10N_DATABASE_COLUMN_CUSTOMER)); + if constexpr (kHasDeadline) ImGui::TableSetupColumn(I18N_TEXT("Deadline", L10N_DATABASE_COLUMN_DEADLINE)); + if constexpr (kHasFactory) ImGui::TableSetupColumn(I18N_TEXT("Factory", L10N_DATABASE_COLUMN_FACTORY)); + if constexpr (kHasOrderTime) ImGui::TableSetupColumn(I18N_TEXT("Order time", L10N_DATABASE_COLUMN_ORDER_TIME)); + if constexpr (kHasCompletionTime) ImGui::TableSetupColumn(I18N_TEXT("Completion time", L10N_DATABASE_COLUMN_DELIVERY_TIME)); + if constexpr (kHasItems) ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS)); + ImGui::TableHeadersRow(); + + if (mActiveFilter) { + // TODO + auto [begin, end] = CalcRangeForPage(mCurrentPageNumber); + end = std::min(end, (int64_t)mActiveEntries.size() - 1); + for (int i = begin; i < end; ++i) { + int rowIdx = mActiveEntries[i]; + DisplayEntry(rowIdx); + } + } else { + int firstRowIdx = mCurrentPageNumber * kMaxEntriesPerPage; + auto& page = *mCurrentPage; + + int end = std::min((int)page.size(), kMaxEntriesPerPage); + for (int i = 0; i < end; ++i) { + DisplayEntry(page[i], i, firstRowIdx + i); + } + } + + ImGui::EndTable(); + } + } + + void DisplayEntry(int rowIdx) + { + // TODO + // auto [pageNumber, pageEntry] = SplitRowIndex(rowIdx); + // auto& entry = LoadAndGetPage(pageNumber)[pageEntry]; + // DisplayEntry(entry, rowIdx); + } + + void DisplayEntry(T& entry, int rowIdx, int entryIdx) + { + ImGui::PushID(rowIdx); + ImGui::TableNextRow(); + + if constexpr (kHasCustomer) { + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.Customer.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) { + mSelectRow = rowIdx; + } + } + + if constexpr (kHasDeadline) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.Deadline.c_str()); + } + + if constexpr (kHasFactory) { + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.Factory.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) { + mSelectRow = rowIdx; + } + } + + if constexpr (kHasOrderTime) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.OrderTime.c_str()); + } + + if constexpr (kHasCompletionTime) { + ImGui::TableNextColumn(); + if (entry.DeliveryTime.empty()) { + ImGui::TextUnformatted(I18N_TEXT("Not delivered", L10N_DATABASE_MESSAGE_NOT_DELIVERED)); + } else { + ImGui::TextUnformatted(entry.DeliveryTime.c_str()); + } + } + + if constexpr (kHasItems) { + ImGui::TableNextColumn(); + if (ImGui::TreeNode(entry.ItemsSummary.c_str())) { + DrawItems(entry.Items); + ImGui::TreePop(); + } + } + + ImGui::PopID(); + } + + void EditEntry(T& entry, int rowIdx, int entryIdx) + { + // TODO + } + + void DisplayDeliveriesTable() + { + if (ImGui::BeginTable("DeliveriesTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { + + ImGui::TableSetupColumn(I18N_TEXT("Shipment time", L10N_DATABASE_COLUMN_SHIPMENT_TIME)); + ImGui::TableSetupColumn(I18N_TEXT("Arrival time", L10N_DATABASE_COLUMN_ARRIVAL_TIME)); + ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS)); + ImGui::TableHeadersRow(); + + auto& entry = (*mCurrentPage)[mSelectRow]; + auto& deliveries = entry.AssociatedDeliveries; + for (auto& delivery : deliveries) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(delivery.ShipmentTime.c_str()); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(delivery.ArriveTime.c_str()); + + ImGui::TableNextColumn(); + if (ImGui::TreeNode(delivery.ItemsSummary.c_str())) { + DrawItems(delivery.Items); + ImGui::TreePop(); + } + } + + ImGui::EndTable(); + } + } + + std::vector<Item> LoadItems(SQLite::Statement& stmt, int64_t id) + { + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, id); + + std::vector<Item> entries; + int itemIdCol = stmt.getColumnIndex("ItemId"); + int countCol = stmt.getColumnIndex("Count"); + while (stmt.executeStep()) { + entries.push_back(Item{ + .ItemId = stmt.getColumn(itemIdCol).getInt(), + .Count = stmt.getColumn(countCol).getInt(), + }); + } + + return entries; + } + + std::string CreateItemsSummary(const std::vector<Item>& items) + { + if (items.empty()) { + return "<empty>"; + } + + std::string result; + for (int i = 0, max = std::min((int)items.size(), kSummaryItemCount); i < max; ++i) { + auto& name = mProject->Products.Find(items[i].ItemId)->GetName(); + if (result.length() + name.length() > kSummaryMaxLength) { + break; + } + + result += name; + result += ", "; + } + + // Remove ", " + result.pop_back(); + result.pop_back(); + + result += "..."; + + return result; + } + + std::vector<DeliveryEntry> LoadDeliveriesEntries(int64_t orderId, DeliveryDirection type) + { + bool outgoingFlag; + switch (type) { + case DeliveryDirection::FactoryToWarehouse: outgoingFlag = false; break; + case DeliveryDirection::WarehouseToCustomer: outgoingFlag = true; break; + } + + auto& stmt = mProject->Database.GetDeliveries().FilterByTypeAndId; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, orderId); + stmt.bind(2, outgoingFlag); + + std::vector<DeliveryEntry> entries; + int rowIdCol = stmt.getColumnIndex("Id"); + int sendTimeCol = stmt.getColumnIndex("ShipmentTime"); + int arrivalTimeCol = stmt.getColumnIndex("ArrivalTime"); + while (stmt.executeStep()) { + auto items = LoadItems( + mProject->Database.GetDeliveries().GetItems, + stmt.getColumn(rowIdCol).getInt64()); + auto summary = CreateItemsSummary(items); + + entries.push_back(DeliveryEntry{ + .Items = std::move(items), + .ItemsSummary = std::move(summary), + .ShipmentTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(arrivalTimeCol).getInt64()), + .ArriveTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(sendTimeCol).getInt64()), + .Direction = type, + }); + } + + return entries; + } + + Page& LoadAndGetPage(int page) + { + auto iter = mPages.find(page); + if (iter != mPages.end()) { + return iter.value(); + } + + auto& stmt = *Statements.GetRows; + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, kMaxEntriesPerPage); + stmt.bind(2, page * kMaxEntriesPerPage); + + // If a field flag is false, the column index won't be used (controlled by other if constexpr's downstream) + // so there is no UB here + + // This column is always necessary (and present) because the deliveries table require it + int idCol = stmt.getColumnIndex("Id"); + + int customerCol; + if constexpr (kHasCustomer) customerCol = stmt.getColumnIndex("Customer"); + + int deadlineCol; + if constexpr (kHasDeadline) deadlineCol = stmt.getColumnIndex("Deadline"); + + int factoryCol; + if constexpr (kHasFactory) factoryCol = stmt.getColumnIndex("Factory"); + + int orderTimeCol; + if constexpr (kHasOrderTime) orderTimeCol = stmt.getColumnIndex("OrderTime"); + + int deliveryTimeCol; + if constexpr (kHasCompletionTime) deliveryTimeCol = stmt.getColumnIndex("DeliveryTime"); + + Page entries; + while (stmt.executeStep()) { + auto& entry = entries.emplace_back(); + + auto id = stmt.getColumn(idCol).getInt64(); + entry.AssociatedDeliveries = LoadDeliveriesEntries(id, T::kType); + + if constexpr (kHasItems) { + auto items = LoadItems( + *Statements.GetItems, + id); + auto itemsSummary = CreateItemsSummary(items); + entry.Items = std::move(items); + entry.ItemsSummary = std::move(itemsSummary); + } + + if constexpr (kHasCustomer) { + auto customerId = stmt.getColumn(customerCol).getInt(); + entry.Customer = mProject->Customers.Find(customerId)->GetName(); + } + + if constexpr (kHasDeadline) { + auto timeStamp = stmt.getColumn(deadlineCol).getInt64(); + entry.Deadline = TimeUtils::StringifyTimeStamp(timeStamp); + } + + if constexpr (kHasFactory) { + auto factoryId = stmt.getColumn(factoryCol).getInt(); + entry.Factory = mProject->Factories.Find(factoryId)->GetName(); + } + + if constexpr (kHasOrderTime) { + auto timeStamp = stmt.getColumn(orderTimeCol).getInt64(); + entry.OrderTime = TimeUtils::StringifyTimeStamp(timeStamp); + } + + if constexpr (kHasCompletionTime) { + auto timeStamp = stmt.getColumn(deliveryTimeCol).getInt64(); + entry.DeliveryTime = TimeUtils::StringifyTimeStamp(timeStamp); + } + } + + auto [res, _] = mPages.try_emplace(page, std::move(entries)); + return res.value(); + } + + void DrawItems(const std::vector<Item>& items) + { + for (auto& item : items) { + auto& name = mProject->Products.Find(item.ItemId)->GetName(); + ImGui::Text("%s × %d", name.c_str(), item.Count); + } + } + + void UpdateLastPage() + { + mLastPage = mActiveEntries.empty() + ? CalcPageForRowId(mRowCount) + : CalcPageForRowId(mActiveEntries.back()); + } +}; + +class SalesTableView : public GenericTableView<SaleEntry> +{ +public: + SalesTableView() + { + mEditDialogTitle = I18N_TEXT("Edit sales entry", L10N_DATABASE_SALES_VIEW_EDIT_DIALOG_TITLE); + } + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + void OnProjectChanged(Project* newProject) + { + auto& table = newProject->Database.GetSales(); + Statements.GetRowCount = &table.GetRowCount; + Statements.GetRows = &table.GetRows; + Statements.GetItems = &table.GetItems; + // TODO + // stmts.FilterRowsStatement = ; + + GenericTableView<SaleEntry>::OnProjectChanged(newProject); + } +#pragma clang diagnostic pop +}; + +class PurchasesTableView : public GenericTableView<PurchaseEntry> +{ +public: + PurchasesTableView() + { + mEditDialogTitle = I18N_TEXT("Edit purchase entry", L10N_DATABASE_PURCHASES_VIEW_EDIT_DIALOG_TITLE); + } + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + void OnProjectChanged(Project* newProject) + { + auto& table = newProject->Database.GetPurchases(); + Statements.GetRowCount = &table.GetRowCount; + Statements.GetRows = &table.GetRows; + Statements.GetItems = &table.GetItems; + // TODO + // stmts.FilterRowsStatement = ; + + GenericTableView<PurchaseEntry>::OnProjectChanged(newProject); + } +#pragma clang diagnostic pop +}; +} // namespace CPLT_UNITY_ID + +void UI::DatabaseViewTab() +{ + auto& gs = GlobalStates::GetInstance(); + + static Project* currentProject = nullptr; + static CPLT_UNITY_ID::SalesTableView sales; + static CPLT_UNITY_ID::PurchasesTableView purchases; + + if (currentProject != gs.GetCurrentProject()) { + currentProject = gs.GetCurrentProject(); + sales.OnProjectChanged(currentProject); + purchases.OnProjectChanged(currentProject); + } + + if (ImGui::BeginTabBar("DatabaseViewTabs")) { + if (ImGui::BeginTabItem(I18N_TEXT("Sales", L10N_DATABASE_SALES_VIEW_TAB_NAME))) { + sales.Display(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Purchases", L10N_DATABASE_PURCHASES_VIEW_TAB_NAME))) { + purchases.Display(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} diff --git a/app/source/Cplt/UI/UI_Items.cpp b/app/source/Cplt/UI/UI_Items.cpp new file mode 100644 index 0000000..0170e1a --- /dev/null +++ b/app/source/Cplt/UI/UI_Items.cpp @@ -0,0 +1,252 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> + +namespace CPLT_UNITY_ID { + +enum class ActionResult +{ + Confirmed, + Canceled, + Pending, +}; + +/// \param list Item list that the item is in. +/// \param item A non-null pointer to the currently being edited item. It should not change until this function returns a non-\c ActionResult::Pending value. +template <class T> +ActionResult ItemEditor(ItemList<T>& list, T* item) +{ + constexpr bool kHasDescription = requires(T t) + { + t.GetDescription(); + }; + constexpr bool kHasEmail = requires(T t) + { + t.GetEmail(); + }; + + static bool duplicateName = false; + + static std::string name; + static std::string description; + static std::string email; + if (name.empty()) { + name = item->GetName(); + if constexpr (kHasDescription) description = item->GetDescription(); + if constexpr (kHasEmail) email = item->GetEmail(); + } + + auto ClearStates = [&]() { + duplicateName = false; + name = {}; + description = {}; + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_ITEM_COLUMN_NAME), &name)) { + duplicateName = name != item->GetName() && list.Find(name) != nullptr; + } + if constexpr (kHasDescription) ImGui::InputText(I18N_TEXT("Description", L10N_ITEM_COLUMN_DESCRIPTION), &description); + if constexpr (kHasEmail) ImGui::InputText(I18N_TEXT("Email", L10N_ITEM_COLUMN_EMAIL), &email); + + if (name.empty()) { + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + } + if (duplicateName) { + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + } + + // Return Value + auto rv = ActionResult::Pending; + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), name.empty() || duplicateName)) { + if (item->GetName() != name) { + item->SetName(std::move(name)); + } + if constexpr (kHasDescription) + if (item->GetDescription() != description) { + item->SetDescription(std::move(description)); + } + if constexpr (kHasEmail) + if (item->GetEmail() != email) { + item->SetEmail(std::move(email)); + } + + ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Confirmed; + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Canceled; + } + + return rv; +} + +template <class T> +void ItemListEntries(ItemList<T>& list, int& selectedIdx) +{ + constexpr bool kHasDescription = requires(T t) + { + t.GetDescription(); + }; + constexpr bool kHasEmail = requires(T t) + { + t.GetEmail(); + }; + constexpr bool kHasStock = requires(T t) + { + t.GetPrice(); + }; + constexpr bool kHasPrice = requires(T t) + { + t.GetPrice(); + }; + constexpr int kColumns = 1 /* Name column */ + kHasDescription + kHasEmail + kHasStock + kHasPrice; + + if (ImGui::BeginTable("", kColumns, ImGuiTableFlags_Borders)) { + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_ITEM_COLUMN_NAME)); + if constexpr (kHasDescription) ImGui::TableSetupColumn(I18N_TEXT("Description", L10N_ITEM_COLUMN_DESCRIPTION)); + if constexpr (kHasEmail) ImGui::TableSetupColumn(I18N_TEXT("Email", L10N_ITEM_COLUMN_EMAIL)); + if constexpr (kHasStock) ImGui::TableSetupColumn(I18N_TEXT("Stock", L10N_ITEM_COLUMN_STOCK)); + if constexpr (kHasPrice) ImGui::TableSetupColumn(I18N_TEXT("Price", L10N_ITEM_COLUMN_PRICE)); + ImGui::TableHeadersRow(); + + size_t idx = 0; + for (auto& entry : list) { + if (entry.IsInvalid()) { + continue; + } + + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(entry.GetName().c_str(), selectedIdx == idx, ImGuiSelectableFlags_SpanAllColumns)) { + selectedIdx = idx; + } + + if constexpr (kHasDescription) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.GetDescription().c_str()); + } + + if constexpr (kHasEmail) { + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.GetEmail().c_str()); + } + + if constexpr (kHasStock) { + ImGui::TableNextColumn(); + ImGui::Text("%d", entry.GetStock()); + } + + if constexpr (kHasPrice) { + ImGui::TableNextColumn(); + // TODO format in dollars + ImGui::Text("%d", entry.GetPrice()); + } + + idx++; + } + ImGui::EndTable(); + } +} + +template <class T> +void ItemListEditor(ItemList<T>& list) +{ + bool opened = true; + static int selectedIdx = -1; + static T* editingItem = nullptr; + + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Add item", L10N_ITEM_ADD_DIALOG_TITLE)); + + editingItem = &list.Insert(""); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Add item", L10N_ITEM_ADD_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + switch (ItemEditor(list, editingItem)) { + case ActionResult::Confirmed: + editingItem = nullptr; + break; + case ActionResult::Canceled: + list.Remove(editingItem->GetId()); + editingItem = nullptr; + break; + default: + break; + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT " " I18N_TEXT("Edit", L10N_EDIT), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Edit item", L10N_ITEM_EDIT_DIALOG_TITLE)); + + editingItem = list.Find(selectedIdx); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Edit item", L10N_ITEM_EDIT_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ItemEditor(list, editingItem) != ActionResult::Pending) { + editingItem = nullptr; + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Delete item", L10N_ITEM_DELETE_DIALOG_TITLE)); + + list.Remove(selectedIdx); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Delete item", L10N_ITEM_DELETE_DIALOG_TITLE), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextUnformatted(I18N_TEXT("Are you sure you want to delete this item?", L10N_ITEM_DELETE_DIALOG_MESSAGE)); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ItemListEntries<T>(list, selectedIdx); +} +} // namespace CPLT_UNITY_ID + +void UI::ItemsTab() +{ + auto& gs = GlobalStates::GetInstance(); + + if (ImGui::BeginTabBar("ItemViewTabs")) { + if (ImGui::BeginTabItem(I18N_TEXT("Products", L10N_ITEM_CATEGORY_PRODUCT))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Products); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Factories", L10N_ITEM_CATEGORY_FACTORY))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Factories); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(I18N_TEXT("Customers", L10N_ITEM_CATEGORY_CUSTOMER))) { + CPLT_UNITY_ID::ItemListEditor(gs.GetCurrentProject()->Customers); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} diff --git a/app/source/Cplt/UI/UI_MainWindow.cpp b/app/source/Cplt/UI/UI_MainWindow.cpp new file mode 100644 index 0000000..4653f79 --- /dev/null +++ b/app/source/Cplt/UI/UI_MainWindow.cpp @@ -0,0 +1,237 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> +#include <portable-file-dialogs.h> +#include <filesystem> +#include <memory> + +namespace fs = std::filesystem; + +namespace CPLT_UNITY_ID { +void ProjectTab_Normal() +{ + auto& gs = GlobalStates::GetInstance(); + + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE))) { + gs.SetCurrentProject(nullptr); + return; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER " " I18N_TEXT("Open in filesystem", L10N_PROJECT_OPEN_IN_FILESYSTEM))) { + // TODO + } + + ImGui::Text("%s %s", I18N_TEXT("Project name", L10N_PROJECT_NAME), gs.GetCurrentProject()->GetName().c_str()); + ImGui::Text("%s %s", I18N_TEXT("Project path", L10N_PROJECT_PATH), gs.GetCurrentProject()->GetPathString().c_str()); +} + +void ProjectTab_NoProject() +{ + auto& gs = GlobalStates::GetInstance(); + + bool openedDummy = true; + bool openErrorDialog = false; + static std::string projectName; + static std::string dirName; + static fs::path dirPath; + static bool dirNameIsValid = false; + + auto TrySelectPath = [&](fs::path newPath) { + if (fs::exists(newPath)) { + dirNameIsValid = true; + dirName = newPath.string(); + dirPath = std::move(newPath); + } else { + dirNameIsValid = false; + } + }; + + if (ImGui::Button(I18N_TEXT("Create project....", L10N_PROJECT_NEW))) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Create project wizard", L10N_PROJECT_NEW_DIALOG_TITLE)); + } + + // Make it so that the modal dialog has a close button + if (ImGui::BeginPopupModal(I18N_TEXT("Create project wizard", L10N_PROJECT_NEW_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::InputTextWithHint("##ProjectName", I18N_TEXT("Project name", L10N_PROJECT_NAME), &projectName); + + if (ImGui::InputTextWithHint("##ProjectPath", I18N_TEXT("Project path", L10N_PROJECT_PATH), &dirName)) { + // Changed, validate value + TrySelectPath(fs::path(dirName)); + } + ImGui::SameLine(); + if (ImGui::Button("...")) { + auto selection = pfd::select_folder(I18N_TEXT("Project path", L10N_PROJECT_NEW_PATH_DIALOG_TITLE)).result(); + if (!selection.empty()) { + TrySelectPath(fs::path(selection)); + } + } + + if (projectName.empty()) { + ImGui::ErrorMessage("%s", I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + } + if (!dirNameIsValid) { + ImGui::ErrorMessage("%s", I18N_TEXT("Invalid path", L10N_INVALID_PATH_ERROR)); + } + + ImGui::Spacing(); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), !dirNameIsValid || projectName.empty())) { + ImGui::CloseCurrentPopup(); + + gs.SetCurrentProject(std::make_unique<Project>(std::move(dirPath), std::move(projectName))); + + // Dialog just got closed, reset states + projectName.clear(); + dirName.clear(); + dirPath.clear(); + dirNameIsValid = false; + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Open project...", L10N_PROJECT_OPEN))) { + auto selection = pfd::open_file(I18N_TEXT("Open project", L10N_PROJECT_OPEN_DIALOG_TITLE)).result(); + if (!selection.empty()) { + fs::path path(selection[0]); + + try { + // Project's constructor wants a path to directory containing cplt_project.json + gs.SetCurrentProject(std::make_unique<Project>(path.parent_path())); + openErrorDialog = false; + } catch (const std::exception& e) { + openErrorDialog = true; + } + } + } + + // TODO cleanup UI + // Recent projects + + ImGui::Separator(); + ImGui::TextUnformatted(I18N_TEXT("Recent projects", L10N_PROJECT_RECENTS)); + + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Clear", L10N_PROJECT_RECENTS_CLEAR))) { + gs.ClearRecentProjects(); + } + + auto& rp = gs.GetRecentProjects(); + // End of vector is the most recently used, so that appending has less overhead + size_t toRemoveIdx = rp.size(); + + if (rp.empty()) { + ImGui::TextUnformatted(I18N_TEXT("No recent projects", L10N_PROJECT_RECENTS_NONE_PRESENT)); + } else { + for (auto it = rp.rbegin(); it != rp.rend(); ++it) { + auto& [path, recent] = *it; + ImGui::TextUnformatted(recent.c_str()); + + size_t idx = std::distance(it, rp.rend()) - 1; + ImGui::PushID(idx); + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN)) { + try { + gs.SetCurrentProject(std::make_unique<Project>(path)); + openErrorDialog = false; + } catch (const std::exception& e) { + openErrorDialog = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Open this project", L10N_PROJECT_RECENTS_OPEN_TOOLTIP)); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH)) { + toRemoveIdx = idx; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Delete this project from the Recent Projects list, the project itself will not be affected", L10N_PROJECT_RECENTS_DELETE_TOOLTIP)); + } + + ImGui::PopID(); + } + } + + if (toRemoveIdx != rp.size()) { + gs.RemoveRecentProject(toRemoveIdx); + } + + if (openErrorDialog) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(I18N_TEXT("Error", L10N_ERROR)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Error", L10N_ERROR), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::ErrorMessage("%s", I18N_TEXT("Invalid project file", L10N_PROJECT_INVALID_PROJECT_FORMAT)); + ImGui::EndPopup(); + } +} +} // namespace CPLT_UNITY_ID + +void UI::MainWindow() +{ + auto& gs = GlobalStates::GetInstance(); + + auto windowSize = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ windowSize.x, windowSize.y }); + ImGui::SetNextWindowPos({ 0, 0 }); + ImGui::Begin("##MainWindow", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); + if (ImGui::BeginTabBar("MainWindowTabs")) { + if (ImGui::BeginTabItem(ICON_FA_COGS " " I18N_TEXT("Settings", L10N_MAIN_TAB_SETTINGS))) { + UI::SettingsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_FILE " " I18N_TEXT("Project", L10N_MAIN_WINDOW_TAB_PROJECT), nullptr)) { + if (gs.HasCurrentProject()) { + CPLT_UNITY_ID::ProjectTab_Normal(); + } else { + CPLT_UNITY_ID::ProjectTab_NoProject(); + } + ImGui::EndTabItem(); + } + if (!gs.HasCurrentProject()) { + // No project open, simply skip all project specific tabs + goto endTab; + } + + if (ImGui::BeginTabItem(ICON_FA_DATABASE " " I18N_TEXT("Data", L10N_MAIN_WINDOW_TAB_DATABASE_VIEW))) { + UI::DatabaseViewTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_BOX " " I18N_TEXT("Items", L10N_MAIN_WINDOW_TAB_ITEMS))) { + UI::ItemsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_SCROLL " " I18N_TEXT("Workflows", L10N_MAIN_WINDOW_TAB_WORKFLOWS))) { + UI::WorkflowsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(ICON_FA_TABLE " " I18N_TEXT("Templates", L10N_MAIN_WINDOW_TAB_TEMPLATES))) { + UI::TemplatesTab(); + ImGui::EndTabItem(); + } + + endTab: + ImGui::EndTabBar(); + } + ImGui::End(); +} diff --git a/app/source/Cplt/UI/UI_Settings.cpp b/app/source/Cplt/UI/UI_Settings.cpp new file mode 100644 index 0000000..71a752a --- /dev/null +++ b/app/source/Cplt/UI/UI_Settings.cpp @@ -0,0 +1,8 @@ +#include <Cplt/UI/UI.hpp> + +#include <imgui.h> + +void UI::SettingsTab() +{ + // TODO +} diff --git a/app/source/Cplt/UI/UI_Templates.cpp b/app/source/Cplt/UI/UI_Templates.cpp new file mode 100644 index 0000000..e01a97d --- /dev/null +++ b/app/source/Cplt/UI/UI_Templates.cpp @@ -0,0 +1,977 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Model/Template/TableTemplateIterator.hpp> +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Utils/I18n.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_extra_math.h> +#include <imgui_internal.h> +#include <imgui_stdlib.h> +#include <charconv> +#include <fstream> +#include <iostream> +#include <utility> +#include <variant> + +namespace CPLT_UNITY_ID { +class TemplateUI +{ +public: + static std::unique_ptr<TemplateUI> CreateByKind(std::unique_ptr<Template> tmpl); + static std::unique_ptr<TemplateUI> CreateByKind(Template::Kind kind); + + virtual ~TemplateUI() = default; + virtual void Display() = 0; + virtual void Close() = 0; +}; + +// Table template styles +constexpr ImU32 kSingleParamOutline = IM_COL32(255, 255, 0, 255); +constexpr ImU32 kArrayGroupOutline = IM_COL32(255, 0, 0, 255); + +class TableTemplateUI : public TemplateUI +{ +private: + std::unique_ptr<TableTemplate> mTable; + + struct UICell + { + bool Hovered = false; + bool Held = false; + bool Selected = false; + }; + std::vector<UICell> mUICells; + + struct UIArrayGroup + { + ImVec2 Pos; + ImVec2 Size; + }; + std::vector<UIArrayGroup> mUIArrayGroups; + + struct Sizer + { + bool Hovered = false; + bool Held = false; + }; + std::vector<Sizer> mRowSizers; + std::vector<Sizer> mColSizers; + + /* Selection range */ + Vec2i mSelectionTL; + Vec2i mSelectionBR; + + /* Selection states */ + + /// "CStates" stands for "Constant cell selection States" + struct CStates + { + }; + + /// "SStates" stands for "Singular parameter selection States". + struct SStates + { + std::string EditBuffer; + bool ErrorDuplicateVarName; + bool HasLeftAG; + bool HasRightAG; + }; + + /// "AStates" stands for "Array group parameter selection States". + struct AStates + { + std::string EditBuffer; + bool ErrorDuplicateVarName; + }; + + // "RStates" stands for "Range selection States". + struct RStates + { + }; + + union + { + // Initialize to this element + std::monostate mIdleState{}; + CStates mCS; + SStates mSS; + AStates mAS; + RStates mRS; + }; + + /* Table resizer dialog states */ + int mNewTableWidth; + int mNewTableHeight; + + /* Table states */ + enum EditMode + { + ModeEditing, + ModeColumnResizing, + ModeRowResizing, + }; + EditMode mMode = ModeEditing; + + float mStartDragDim; + /// Depending on row/column sizer being dragged, this will be the y/x coordinate + float mStartDragMouseCoordinate; + + bool mDirty = false; + bool mFirstDraw = true; + +public: + TableTemplateUI(std::unique_ptr<TableTemplate> table) + : mTable{ std::move(table) } + , mSelectionTL{ -1, -1 } + , mSelectionBR{ -1, -1 } + { + // TODO debug code + Resize(6, 5); + } + + ~TableTemplateUI() override + { + // We can't move this to be a destructor of the union + // because that way it would run after the destruction of mTable + if (!IsSelected()) { + // Case: mIdleState + // Noop + } else if (mSelectionTL == mSelectionBR) { + switch (mTable->GetCell(mSelectionTL).Type) { + case TableCell::ConstantCell: + // Case: mCS + // Noop + break; + + case TableCell::SingularParametricCell: + // Case: mSS + mSS.EditBuffer.std::string::~string(); + break; + + case TableCell::ArrayParametricCell: + // Case: mAS + mAS.EditBuffer.std::string::~string(); + break; + } + } else { + // Case: mRS + // Noop + } + } + + void Display() override + { + ImGui::Columns(2); + if (mFirstDraw) { + mFirstDraw = false; + ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.15f); + } + + DisplayInspector(); + ImGui::NextColumn(); + + auto initialPos = ImGui::GetCursorPos(); + DisplayTable(); + DisplayTableResizers(initialPos); + ImGui::NextColumn(); + + ImGui::Columns(1); + } + + void Close() override + { + // TODO + } + + void Resize(int width, int height) + { + mTable->Resize(width, height); + mUICells.resize(width * height); + mUIArrayGroups.resize(mTable->GetArrayGroupCount()); + mRowSizers.resize(width); + mColSizers.resize(height); + + for (size_t i = 0; i < mUIArrayGroups.size(); ++i) { + auto& ag = mTable->GetArrayGroup(i); + auto& uag = mUIArrayGroups[i]; + + auto itemSpacing = ImGui::GetStyle().ItemSpacing; + uag.Pos.x = CalcTablePixelWidth() + itemSpacing.x; + uag.Pos.y = CalcTablePixelHeight() + itemSpacing.y; + + uag.Size.x = mTable->GetRowHeight(ag.Row); + uag.Size.y = 0; + for (int x = ag.LeftCell; x <= ag.RightCell; ++x) { + uag.Size.y += mTable->GetColumnWidth(x); + } + } + + mSelectionTL = { 0, 0 }; + mSelectionBR = { 0, 0 }; + } + +private: + void DisplayInspector() + { + bool openedDummy = true; + + // This is an id, no need to localize + if (ImGui::BeginTabBar("Inspector")) { + if (ImGui::BeginTabItem(I18N_TEXT("Cell", L10N_TABLE_CELL))) { + if (!IsSelected()) { + ImGui::Text(I18N_TEXT("Select a cell to edit", L10N_TABLE_CELL_SELECT_MSG)); + } else if (mSelectionTL == mSelectionBR) { + DisplayCellProperties(mSelectionTL); + } else { + DisplayRangeProperties(mSelectionTL, mSelectionBR); + } + ImGui::EndTabItem(); + } + + auto OpenPopup = [](const char* name) { + // Act as if ImGui::OpenPopup is executed in the previous id stack frame (tab bar level) + // Note: we can't simply use ImGui::GetItemID() here, because that would return the id of the ImGui::Button + auto tabBar = ImGui::GetCurrentContext()->CurrentTabBar; + auto id = tabBar->Tabs[tabBar->LastTabItemIdx].ID; + ImGui::PopID(); + ImGui::OpenPopup(name); + ImGui::PushOverrideID(id); + }; + if (ImGui::BeginTabItem(I18N_TEXT("Table", L10N_TABLE))) { + if (ImGui::Button(I18N_TEXT("Configure table properties...", L10N_TABLE_CONFIGURE_PROPERTIES))) { + mNewTableWidth = mTable->GetTableWidth(); + mNewTableHeight = mTable->GetTableHeight(); + OpenPopup(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES)); + } + + int mode = mMode; + ImGui::RadioButton(I18N_TEXT("Edit table", L10N_TABLE_EDIT_TABLE), &mode, ModeEditing); + ImGui::RadioButton(I18N_TEXT("Resize column widths", L10N_TABLE_EDIT_RESIZE_COLS), &mode, ModeColumnResizing); + ImGui::RadioButton(I18N_TEXT("Resize rows heights", L10N_TABLE_EDIT_RESIZE_ROWS), &mode, ModeRowResizing); + mMode = static_cast<EditMode>(mode); + + // Table contents + DisplayTableContents(); + + ImGui::EndTabItem(); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + DisplayTableProperties(); + ImGui::EndPopup(); + } + + ImGui::EndTabBar(); + } + } + + static char NthUppercaseLetter(int n) + { + return (char)((int)'A' + n); + } + + static void ExcelRow(int row, char* bufferBegin, char* bufferEnd) + { + auto res = std::to_chars(bufferBegin, bufferEnd, row); + if (res.ec != std::errc()) { + return; + } + } + + static char* ExcelColumn(int column, char* bufferBegin, char* bufferEnd) + { + // https://stackoverflow.com/a/182924/11323702 + + int dividend = column; + int modulo; + + char* writeHead = bufferEnd - 1; + *writeHead = '\0'; + --writeHead; + + while (dividend > 0) { + if (writeHead < bufferBegin) { + return nullptr; + } + + modulo = (dividend - 1) % 26; + + *writeHead = NthUppercaseLetter(modulo); + --writeHead; + + dividend = (dividend - modulo) / 26; + } + + // `writeHead` at this point would be a one-past-the-bufferEnd reverse iterator (i.e. one-past-the-(text)beginning in the bufferBegin) + // add 1 to get to the actual beginning of the text + return writeHead + 1; + } + + void DisplayCellProperties(Vec2i pos) + { + auto& cell = mTable->GetCell(pos); + auto& uiCell = mUICells[pos.y * mTable->GetTableWidth() + pos.x]; + + char colStr[8]; // 2147483647 -> FXSHRXW, len == 7, along with \0 + char* colBegin = ExcelColumn(pos.x + 1, std::begin(colStr), std::end(colStr)); + char rowStr[11]; // len(2147483647) == 10, along with \0 + ExcelRow(pos.y + 1, std::begin(rowStr), std::end(rowStr)); + ImGui::Text(I18N_TEXT("Location: %s%s", L10N_TABLE_CELL_POS), colBegin, rowStr); + + switch (cell.Type) { + case TableCell::ConstantCell: + ImGui::Text(I18N_TEXT("Type: Constant", L10N_TABLE_CELL_TYPE_CONST)); + break; + case TableCell::SingularParametricCell: + ImGui::Text(I18N_TEXT("Type: Single parameter", L10N_TABLE_CELL_TYPE_PARAM)); + break; + case TableCell::ArrayParametricCell: + ImGui::Text(I18N_TEXT("Type: Array group", L10N_TABLE_CELL_TYPE_CREATE_AG)); + break; + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT)) { + ImGui::OpenPopup("ConvertCtxMenu"); + } + if (ImGui::BeginPopup("ConvertCtxMenu")) { + bool constantEnabled = cell.Type != TableCell::ConstantCell; + if (ImGui::MenuItem(I18N_TEXT("Convert to regular cell", L10N_TABLE_CELL_CONV_CONST), nullptr, false, constantEnabled)) { + mTable->SetCellType(pos, TableCell::ConstantCell); + ResetCS(); + } + + bool singleEnabled = cell.Type != TableCell::SingularParametricCell; + if (ImGui::MenuItem(I18N_TEXT("Convert to parameter cell", L10N_TABLE_CELL_CONV_PARAM), nullptr, false, singleEnabled)) { + mTable->SetCellType(pos, TableCell::SingularParametricCell); + ResetSS(pos); + } + + bool arrayEnabled = cell.Type != TableCell::ArrayParametricCell; + if (ImGui::MenuItem(I18N_TEXT("Add to a new array group", L10N_TABLE_CELL_CONV_CREATE_AG), nullptr, false, arrayEnabled)) { + mTable->AddArrayGroup(pos.y, pos.x, pos.x); // Use automatically generated name + ResetAS(pos); + } + + bool leftEnabled = mSS.HasLeftAG && arrayEnabled; + if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the left", L10N_TABLE_CELL_CONV_ADD_AG_LEFT), nullptr, false, leftEnabled)) { + auto& leftCell = mTable->GetCell({ pos.x - 1, pos.y }); + mTable->ExtendArrayGroupRight(leftCell.DataId, 1); + ResetAS(pos); + } + + bool rightEnabled = mSS.HasRightAG && arrayEnabled; + if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the right", L10N_TABLE_CELL_CONV_ADD_AG_RIGHT), nullptr, false, rightEnabled)) { + auto& rightCell = mTable->GetCell({ pos.x + 1, pos.y }); + mTable->ExtendArrayGroupLeft(rightCell.DataId, 1); + ResetAS(pos); + } + + ImGui::EndPopup(); + } + + ImGui::Spacing(); + + constexpr auto kLeft = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_LEFT); + constexpr auto kCenter = I18N_TEXT("Center", L10N_TABLE_CELL_ALIGN_CENTER); + constexpr auto kRight = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_RIGHT); + + const char* horizontalText; + switch (cell.HorizontalAlignment) { + case TableCell::AlignAxisMin: horizontalText = kLeft; break; + case TableCell::AlignCenter: horizontalText = kCenter; break; + case TableCell::AlignAxisMax: horizontalText = kRight; break; + } + + if (ImGui::BeginCombo(I18N_TEXT("Horizontal alignment", L10N_TABLE_CELL_HORIZONTAL_ALIGNMENT), horizontalText)) { + if (ImGui::Selectable(kLeft, cell.HorizontalAlignment == TableCell::AlignAxisMin)) { + cell.HorizontalAlignment = TableCell::AlignAxisMin; + } + if (ImGui::Selectable(kCenter, cell.HorizontalAlignment == TableCell::AlignCenter)) { + cell.HorizontalAlignment = TableCell::AlignCenter; + } + if (ImGui::Selectable(kRight, cell.HorizontalAlignment == TableCell::AlignAxisMax)) { + cell.HorizontalAlignment = TableCell::AlignAxisMax; + } + ImGui::EndCombo(); + } + + constexpr auto kTop = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_TOP); + constexpr auto kMiddle = I18N_TEXT("Middle", L10N_TABLE_CELL_ALIGN_MIDDLE); + constexpr auto kBottom = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_BOTTOM); + + const char* verticalText; + switch (cell.VerticalAlignment) { + case TableCell::AlignAxisMin: verticalText = kTop; break; + case TableCell::AlignCenter: verticalText = kMiddle; break; + case TableCell::AlignAxisMax: verticalText = kBottom; break; + } + + if (ImGui::BeginCombo(I18N_TEXT("Vertical alignment", L10N_TABLE_CELL_VERTICAL_ALIGNMENT), verticalText)) { + if (ImGui::Selectable(kTop, cell.VerticalAlignment == TableCell::AlignAxisMin)) { + cell.VerticalAlignment = TableCell::AlignAxisMin; + } + if (ImGui::Selectable(kMiddle, cell.VerticalAlignment == TableCell::AlignCenter)) { + cell.VerticalAlignment = TableCell::AlignCenter; + } + if (ImGui::Selectable(kBottom, cell.VerticalAlignment == TableCell::AlignAxisMax)) { + cell.VerticalAlignment = TableCell::AlignAxisMax; + } + ImGui::EndCombo(); + } + + switch (cell.Type) { + case TableCell::ConstantCell: + ImGui::InputText(I18N_TEXT("Content", L10N_TABLE_CELL_CONTENT), &cell.Content); + break; + + case TableCell::SingularParametricCell: + if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mSS.EditBuffer)) { + // Sync name change to table + bool success = mTable->UpdateParameterName(cell.Content, mSS.EditBuffer); + if (success) { + // Flush name to display content + cell.Content = mSS.EditBuffer; + mSS.ErrorDuplicateVarName = false; + } else { + mSS.ErrorDuplicateVarName = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell.", L10N_TABLE_CELL_VAR_TOOLTIP)); + } + + if (mSS.ErrorDuplicateVarName) { + ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP)); + } + break; + + case TableCell::ArrayParametricCell: + if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mAS.EditBuffer)) { + auto ag = mTable->GetArrayGroup(cell.DataId); + bool success = ag.UpdateCellName(cell.Content, mAS.EditBuffer); + if (success) { + cell.Content = mAS.EditBuffer; + mAS.ErrorDuplicateVarName = false; + } else { + mAS.ErrorDuplicateVarName = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell; local within the array group.", L10N_TABLE_CELL_ARRAY_VAR_TOOLTIP)); + } + + if (mAS.ErrorDuplicateVarName) { + ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP)); + } + break; + } + } + + void DisplayRangeProperties(Vec2i tl, Vec2i br) + { + // TODO + } + + void DisplayTableContents() + { + if (ImGui::TreeNode(ICON_FA_BONG " " I18N_TEXT("Parameters", L10N_TABLE_SINGLE_PARAMS))) { + TableSingleParamsIter iter(*mTable); + while (iter.HasNext()) { + auto& cell = iter.Next(); + if (ImGui::Selectable(cell.Content.c_str())) { + SelectCell(cell.Location); + } + } + ImGui::TreePop(); + } + if (ImGui::TreeNode(ICON_FA_LIST " " I18N_TEXT("Array groups", L10N_TABLE_ARRAY_GROUPS))) { + TableArrayGroupsIter iter(*mTable); + // For each array group + while (iter.HasNext()) { + if (ImGui::TreeNode(iter.PeekNameCStr())) { + auto& ag = iter.Peek(); + // For each cell in the array group + for (int x = ag.LeftCell; x <= ag.RightCell; ++x) { + Vec2i pos{ x, ag.Row }; + auto& cell = mTable->GetCell(pos); + if (ImGui::Selectable(cell.Content.c_str())) { + SelectCell(pos); + } + } + ImGui::TreePop(); + } + iter.Next(); + } + ImGui::TreePop(); + } + } + + void DisplayTableProperties() + { + ImGui::InputInt(I18N_TEXT("Width", L10N_TABLE_WIDTH), &mNewTableWidth); + ImGui::InputInt(I18N_TEXT("Height", L10N_TABLE_HEIGHT), &mNewTableHeight); + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + Resize(mNewTableWidth, mNewTableHeight); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + // TODO + } + + void DisplayTable() + { + struct CellPalette + { + ImU32 Regular; + ImU32 Hovered; + ImU32 Active; + + ImU32 GetColorFor(const UICell& cell) const + { + if (cell.Held) { + return Active; + } else if (cell.Hovered) { + return Hovered; + } else { + return Regular; + } + } + }; + + CellPalette constantPalette{ + .Regular = ImGui::GetColorU32(ImGuiCol_Button), + .Hovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered), + .Active = ImGui::GetColorU32(ImGuiCol_ButtonActive), + }; + CellPalette paramPalette{ + .Regular = IM_COL32(0, 214, 4, 102), + .Hovered = IM_COL32(0, 214, 4, 255), + .Active = IM_COL32(0, 191, 2, 255), + }; + + // TODO array group color + + auto navHighlight = ImGui::GetColorU32(ImGuiCol_NavHighlight); + + int colCount = mTable->GetTableWidth(); + int rowCount = mTable->GetTableHeight(); + for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) { + int rowHeight = mTable->GetRowHeight(rowIdx); + + for (int colIdx = 0; colIdx < colCount; ++colIdx) { + int colWidth = mTable->GetColumnWidth(colIdx); + + int i = rowIdx * colCount + colIdx; + auto window = ImGui::GetCurrentWindow(); + auto id = window->GetID(i); + + Vec2i cellLoc{ colIdx, rowIdx }; + auto& cell = mTable->GetCell(cellLoc); + auto& uiCell = mUICells[i]; + + ImVec2 size(colWidth, rowHeight); + ImRect rect{ + window->DC.CursorPos, + window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), + }; + + /* Draw cell selection */ + + if (uiCell.Selected) { + constexpr int mt = 2; // Marker Thickness + constexpr int ms = 8; // Marker Size + + ImVec2 outerTL(rect.Min - ImVec2(mt, mt)); + ImVec2 outerBR(rect.Max + ImVec2(mt, mt)); + + // Top left + window->DrawList->AddRectFilled(outerTL + ImVec2(0, 0), outerTL + ImVec2(ms, mt), navHighlight); + window->DrawList->AddRectFilled(outerTL + ImVec2(0, mt), outerTL + ImVec2(mt, ms), navHighlight); + + // Top right + ImVec2 outerTR(outerBR.x, outerTL.y); + window->DrawList->AddRectFilled(outerTR + ImVec2(-ms, 0), outerTR + ImVec2(0, mt), navHighlight); + window->DrawList->AddRectFilled(outerTR + ImVec2(-mt, mt), outerTR + ImVec2(0, ms), navHighlight); + + // Bottom right + window->DrawList->AddRectFilled(outerBR + ImVec2(-ms, -mt), outerBR + ImVec2(0, 0), navHighlight); + window->DrawList->AddRectFilled(outerBR + ImVec2(-mt, -ms), outerBR + ImVec2(0, -mt), navHighlight); + + // Bottom left + ImVec2 outerBL(outerTL.x, outerBR.y); + window->DrawList->AddRectFilled(outerBL + ImVec2(0, -mt), outerBL + ImVec2(ms, 0), navHighlight); + window->DrawList->AddRectFilled(outerBL + ImVec2(0, -ms), outerBL + ImVec2(mt, -mt), navHighlight); + } + + /* Draw cell body */ + + CellPalette* palette; + switch (cell.Type) { + case TableCell::ConstantCell: + palette = &constantPalette; + break; + + case TableCell::SingularParametricCell: + case TableCell::ArrayParametricCell: + palette = ¶mPalette; + break; + } + + window->DrawList->AddRectFilled(rect.Min, rect.Max, palette->GetColorFor(uiCell)); + + /* Draw cell content */ + + auto content = cell.Content.c_str(); + auto contentEnd = content + cell.Content.size(); + auto textSize = ImGui::CalcTextSize(content, contentEnd); + + ImVec2 textRenderPos; + switch (cell.HorizontalAlignment) { + case TableCell::AlignAxisMin: textRenderPos.x = rect.Min.x; break; + case TableCell::AlignCenter: textRenderPos.x = rect.Min.x + colWidth / 2 - textSize.x / 2; break; + case TableCell::AlignAxisMax: textRenderPos.x = rect.Max.x - textSize.x; break; + } + switch (cell.VerticalAlignment) { + case TableCell::AlignAxisMin: textRenderPos.y = rect.Min.y; break; + case TableCell::AlignCenter: textRenderPos.y = rect.Min.y + rowHeight / 2 - textSize.y / 2; break; + case TableCell::AlignAxisMax: textRenderPos.y = rect.Max.y - textSize.y; break; + } + window->DrawList->AddText(textRenderPos, IM_COL32(0, 0, 0, 255), content, contentEnd); + + /* Update ImGui cursor */ + + ImGui::ItemSize(size); + if (!ImGui::ItemAdd(rect, id)) { + goto logicEnd; + } + + if (mMode != ModeEditing) { + goto logicEnd; + } + if (ImGui::ButtonBehavior(rect, id, &uiCell.Hovered, &uiCell.Held)) { + if (ImGui::GetIO().KeyShift && IsSelected()) { + SelectRange(mSelectionTL, { colIdx, rowIdx }); + } else { + SelectCell({ colIdx, rowIdx }); + } + } + + logicEnd: + // Don't SameLine() if we are on the last cell in the row + if (colIdx != colCount - 1) { + ImGui::SameLine(); + } + } + } + + for (auto& uag : mUIArrayGroups) { + ImGui::GetCurrentWindow()->DrawList->AddRect( + uag.Pos - ImVec2(1, 1), + uag.Pos + uag.Size + ImVec2(1, 1), + kArrayGroupOutline); + } + } + + void DisplayResizers( + ImVec2 pos, + ImVec2 sizerDim, + ImVec2 sizerOffset, + std::type_identity_t<float ImVec2::*> vecCompGetter, + std::type_identity_t<int (TableTemplate::*)() const> lenGetter, + std::type_identity_t<int (TableTemplate::*)(int) const> dimGetter, + std::type_identity_t<void (TableTemplate::*)(int, int)> dimSetter) + { + auto window = ImGui::GetCurrentWindow(); + auto spacing = ImGui::GetStyle().ItemSpacing.*vecCompGetter; + + auto regularColor = ImGui::GetColorU32(ImGuiCol_Button); + auto hoveredColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered); + auto activeColor = ImGui::GetColorU32(ImGuiCol_ButtonActive); + + auto GetColor = [&](const Sizer& sizer) -> ImU32 { + if (sizer.Held) { + return activeColor; + } else if (sizer.Hovered) { + return hoveredColor; + } else { + return regularColor; + } + }; + + for (int ix = 0; ix < (mTable.get()->*lenGetter)(); ++ix) { + // ImGui uses float for sizes, our table uses int (because excel uses int) + // Convert here to avoid mountains of narrowing warnings below + auto dim = (float)(mTable.get()->*dimGetter)(ix); + + pos.*vecCompGetter += dim; + ImRect rect{ + pos - sizerOffset, + pos - sizerOffset + ImGui::CalcItemSize(sizerDim, 0.0f, 0.0f), + }; + pos.*vecCompGetter += spacing; + + auto& sizer = mColSizers[ix]; + auto id = window->GetID(ix); + window->DrawList->AddRectFilled(rect.Min, rect.Max, GetColor(sizer)); + + if (ImGui::ButtonBehavior(rect, id, &sizer.Hovered, &sizer.Held, ImGuiButtonFlags_PressedOnClick)) { + mStartDragDim = dim; + mStartDragMouseCoordinate = ImGui::GetMousePos().*vecCompGetter; + } + if (sizer.Held) { + float change = ImGui::GetMousePos().*vecCompGetter - mStartDragMouseCoordinate; + float colWidth = std::max(mStartDragDim + change, 1.0f); + (mTable.get()->*dimSetter)(ix, (int)colWidth); + } + } + } + + void DisplayTableResizers(ImVec2 topLeftPixelPos) + { + constexpr float kExtraSideLength = 5.0f; + constexpr float kExtraAxialLength = 2.0f; + + switch (mMode) { + case ModeEditing: break; + + case ModeColumnResizing: + ImGui::PushID("Cols"); + DisplayResizers( + topLeftPixelPos, + ImVec2( + ImGui::GetStyle().ItemSpacing.x + kExtraSideLength * 2, + CalcTablePixelHeight() + kExtraAxialLength * 2), + ImVec2(kExtraSideLength, kExtraAxialLength), + &ImVec2::x, + &TableTemplate::GetTableWidth, + &TableTemplate::GetColumnWidth, + &TableTemplate::SetColumnWidth); + ImGui::PopID(); + break; + + case ModeRowResizing: + ImGui::PushID("Rows"); + DisplayResizers( + topLeftPixelPos, + ImVec2( + CalcTablePixelWidth() + kExtraAxialLength * 2, + ImGui::GetStyle().ItemSpacing.y + kExtraSideLength * 2), + ImVec2(kExtraAxialLength, kExtraSideLength), + &ImVec2::y, + &TableTemplate::GetTableHeight, + &TableTemplate::GetRowHeight, + &TableTemplate::SetRowHeight); + ImGui::PopID(); + break; + } + } + + float CalcTablePixelWidth() const + { + float horizontalSpacing = ImGui::GetStyle().ItemSpacing.x; + float width = 0; + for (int x = 0; x < mTable->GetTableWidth(); ++x) { + width += mTable->GetColumnWidth(x); + width += horizontalSpacing; + } + return width - horizontalSpacing; + } + + float CalcTablePixelHeight() const + { + float verticalSpacing = ImGui::GetStyle().ItemSpacing.y; + float height = 0; + for (int y = 0; y < mTable->GetTableHeight(); ++y) { + height += mTable->GetRowHeight(y); + height += verticalSpacing; + } + return height - verticalSpacing; + } + + template <class TFunction> + void ForeachSelectedCell(const TFunction& func) + { + for (int y = mSelectionTL.y; y <= mSelectionBR.y; ++y) { + for (int x = mSelectionTL.x; x <= mSelectionBR.x; ++x) { + int i = y * mTable->GetTableWidth() + x; + func(i, x, y); + } + } + } + + bool IsSelected() const + { + return mSelectionTL.x != -1; + } + + void ClearSelection() + { + if (IsSelected()) { + ForeachSelectedCell([this](int i, int, int) { + auto& uiCell = mUICells[i]; + uiCell.Selected = false; + }); + } + + mSelectionTL = { -1, -1 }; + mSelectionBR = { -1, -1 }; + + ResetIdleState(); + } + + void ResetIdleState() + { + mIdleState = {}; + } + + void SelectRange(Vec2i p1, Vec2i p2) + { + ClearSelection(); + + if (p2.x < p1.x) { + std::swap(p2.x, p1.x); + } + if (p2.y < p1.y) { + std::swap(p2.y, p1.y); + } + + mSelectionTL = p1; + mSelectionBR = p2; + + ForeachSelectedCell([this](int i, int, int) { + auto& uiCell = mUICells[i]; + uiCell.Selected = true; + }); + + ResetRS(); + } + + void ResetRS() + { + mRS = {}; + } + + void SelectCell(Vec2i pos) + { + ClearSelection(); + + mSelectionTL = pos; + mSelectionBR = pos; + + int i = pos.y * mTable->GetTableWidth() + pos.x; + mUICells[i].Selected = true; + + switch (mTable->GetCell(pos).Type) { + case TableCell::ConstantCell: ResetCS(); break; + case TableCell::SingularParametricCell: ResetSS(pos); break; + case TableCell::ArrayParametricCell: ResetAS(pos); break; + } + } + + void ResetCS() + { + mCS = {}; + } + + void ResetSS(Vec2i pos) + { + new (&mSS) SStates{ + .EditBuffer = mTable->GetCell(pos).Content, + .ErrorDuplicateVarName = false, + .HasLeftAG = pos.x > 1 && mTable->GetCell({ pos.x - 1, pos.y }).Type == TableCell::ArrayParametricCell, + .HasRightAG = pos.x < mTable->GetTableWidth() - 1 && mTable->GetCell({ pos.x + 1, pos.y }).Type == TableCell::ArrayParametricCell, + }; + } + + void ResetAS(Vec2i pos) + { + new (&mAS) AStates{ + .EditBuffer = mTable->GetCell(pos).Content, + .ErrorDuplicateVarName = false, + }; + } +}; + +template <class TTarget> +static auto CastTemplateAs(std::unique_ptr<Template>& input) requires std::is_base_of_v<Template, TTarget> +{ + return std::unique_ptr<TTarget>(static_cast<TTarget*>(input.release())); +} + +std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(std::unique_ptr<Template> tmpl) +{ + switch (tmpl->GetKind()) { + case Template::KD_Table: return std::make_unique<TableTemplateUI>(CastTemplateAs<TableTemplate>(tmpl)); + case Template::InvalidKind: break; + } + return nullptr; +} + +std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(Template::Kind kind) +{ + switch (kind) { + case Template::KD_Table: return std::make_unique<TableTemplateUI>(std::make_unique<TableTemplate>()); + case Template::InvalidKind: break; + } + return nullptr; +} +} // namespace CPLT_UNITY_ID + +void UI::TemplatesTab() +{ + auto& project = *GlobalStates::GetInstance().GetCurrentProject(); + + static std::unique_ptr<CPLT_UNITY_ID::TemplateUI> openTemplate; + static AssetList::ListState state; + bool openedDummy = true; + + // Toolbar item: close + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE), openTemplate == nullptr)) { + openTemplate->Close(); + openTemplate = nullptr; + } + + // Toolbar item: open... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Open asset...", L10N_ASSET_OPEN))) { + ImGui::OpenPopup(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(ICON_FA_FOLDER_OPEN " " I18N_TEXT("Open", L10N_OPEN), state.SelectedAsset == nullptr)) { + ImGui::CloseCurrentPopup(); + + auto tmpl = project.Templates.Load(*state.SelectedAsset); + openTemplate = CPLT_UNITY_ID::TemplateUI::CreateByKind(std::move(tmpl)); + } + ImGui::SameLine(); + project.Templates.DisplayControls(state); + project.Templates.DisplayDetailsList(state); + + ImGui::EndPopup(); + } + + // Toolbar item: manage... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Manage assets...", L10N_ASSET_MANAGE))) { + ImGui::OpenPopup(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + project.Templates.DisplayControls(state); + project.Templates.DisplayDetailsList(state); + ImGui::EndPopup(); + } + + if (openTemplate) { + openTemplate->Display(); + } +} diff --git a/app/source/Cplt/UI/UI_Utils.cpp b/app/source/Cplt/UI/UI_Utils.cpp new file mode 100644 index 0000000..a2bf692 --- /dev/null +++ b/app/source/Cplt/UI/UI_Utils.cpp @@ -0,0 +1,315 @@ +#include "UI.hpp" + +#include <IconsFontAwesome.h> +#include <imgui.h> + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond) +{ + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ vs.x * xPercent, vs.y * yPercent }, cond); +} + +void ImGui::SetNextWindowCentered(ImGuiCond cond) +{ + auto vs = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, cond, { 0.5f, 0.5f }); +} + +void ImGui::PushDisabled() +{ + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha); +} + +void ImGui::PopDisabled() +{ + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); +} + +bool ImGui::Button(const char* label, bool disabled) +{ + return Button(label, ImVec2{}, disabled); +} + +bool ImGui::Button(const char* label, const ImVec2& sizeArg, bool disabled) +{ + if (disabled) PushDisabled(); + bool res = ImGui::Button(label, sizeArg); + if (disabled) PopDisabled(); + + // Help clang-tidy's static analyzer: if the button is disabled, res should always be false + assert(!disabled || (disabled && !res)); + + return res; +} + +void ImGui::ErrorIcon() +{ + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 237 / 255.0f, 67 / 255.0f, 55 / 255.0f, 1.0f }); // #ED4337 + ImGui::Text(ICON_FA_EXCLAMATION_CIRCLE); + ImGui::PopStyleColor(); +} + +void ImGui::ErrorMessage(const char* fmt, ...) +{ + ErrorIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::WarningIcon() +{ + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 255 / 255.0f, 184 / 255.0f, 24 / 255.0f, 1.0f }); // #FFB818 + ImGui::Text(ICON_FA_EXCLAMATION_TRIANGLE); + ImGui::PopStyleColor(); +} + +void ImGui::WarningMessage(const char* fmt, ...) +{ + WarningIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::DrawIcon(ImDrawList* drawList, const ImVec2& a, const ImVec2& b, IconType type, bool filled, ImU32 color, ImU32 innerColor) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/drawing.cpp + // ax::NodeEditor::DrawIcon + + // Brace style was adapted but no names are changed + + auto rect = ImRect(a, b); + auto rect_x = rect.Min.x; + auto rect_y = rect.Min.y; + auto rect_w = rect.Max.x - rect.Min.x; + auto rect_h = rect.Max.y - rect.Min.y; + auto rect_center_x = (rect.Min.x + rect.Max.x) * 0.5f; + auto rect_center_y = (rect.Min.y + rect.Max.y) * 0.5f; + auto rect_center = ImVec2(rect_center_x, rect_center_y); + const auto outline_scale = rect_w / 24.0f; + const auto extra_segments = static_cast<int>(2 * outline_scale); // for full circle + + if (type == IconType::Flow) { + const auto origin_scale = rect_w / 24.0f; + + const auto offset_x = 1.0f * origin_scale; + const auto offset_y = 0.0f * origin_scale; + const auto margin = 2.0f * origin_scale; + const auto rounding = 0.1f * origin_scale; + const auto tip_round = 0.7f; // percentage of triangle edge (for tip) + //const auto edge_round = 0.7f; // percentage of triangle edge (for corner) + const auto canvas = ImRect( + rect.Min.x + margin + offset_x, + rect.Min.y + margin + offset_y, + rect.Max.x - margin + offset_x, + rect.Max.y - margin + offset_y); + const auto canvas_x = canvas.Min.x; + const auto canvas_y = canvas.Min.y; + const auto canvas_w = canvas.Max.x - canvas.Min.x; + const auto canvas_h = canvas.Max.y - canvas.Min.y; + + const auto left = canvas_x + canvas_w * 0.5f * 0.3f; + const auto right = canvas_x + canvas_w - canvas_w * 0.5f * 0.3f; + const auto top = canvas_y + canvas_h * 0.5f * 0.2f; + const auto bottom = canvas_y + canvas_h - canvas_h * 0.5f * 0.2f; + const auto center_y = (top + bottom) * 0.5f; + //const auto angle = AX_PI * 0.5f * 0.5f * 0.5f; + + const auto tip_top = ImVec2(canvas_x + canvas_w * 0.5f, top); + const auto tip_right = ImVec2(right, center_y); + const auto tip_bottom = ImVec2(canvas_x + canvas_w * 0.5f, bottom); + + drawList->PathLineTo(ImVec2(left, top) + ImVec2(0, rounding)); + drawList->PathBezierCurveTo( + ImVec2(left, top), + ImVec2(left, top), + ImVec2(left, top) + ImVec2(rounding, 0)); + drawList->PathLineTo(tip_top); + drawList->PathLineTo(tip_top + (tip_right - tip_top) * tip_round); + drawList->PathBezierCurveTo( + tip_right, + tip_right, + tip_bottom + (tip_right - tip_bottom) * tip_round); + drawList->PathLineTo(tip_bottom); + drawList->PathLineTo(ImVec2(left, bottom) + ImVec2(rounding, 0)); + drawList->PathBezierCurveTo( + ImVec2(left, bottom), + ImVec2(left, bottom), + ImVec2(left, bottom) - ImVec2(0, rounding)); + + if (!filled) { + if (innerColor & 0xFF000000) { + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + } + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } else { + drawList->PathFillConvex(color); + } + } else { + auto triangleStart = rect_center_x + 0.32f * rect_w; + + auto rect_offset = -static_cast<int>(rect_w * 0.25f * 0.25f); + + rect.Min.x += rect_offset; + rect.Max.x += rect_offset; + rect_x += rect_offset; + rect_center_x += rect_offset * 0.5f; + rect_center.x += rect_offset * 0.5f; + + if (type == IconType::Circle) { + const auto c = rect_center; + + if (!filled) { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + + if (innerColor & 0xFF000000) + drawList->AddCircleFilled(c, r, innerColor, 12 + extra_segments); + drawList->AddCircle(c, r, color, 12 + extra_segments, 2.0f * outline_scale); + } else { + drawList->AddCircleFilled(c, 0.5f * rect_w / 2.0f, color, 12 + extra_segments); + } + } + + if (type == IconType::Square) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, 0, 15 + extra_segments); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, 0, 15 + extra_segments); + + drawList->AddRect(p0, p1, color, 0, 15 + extra_segments, 2.0f * outline_scale); + } + } + + if (type == IconType::Grid) { + const auto r = 0.5f * rect_w / 2.0f; + const auto w = ceilf(r / 3.0f); + + const auto baseTl = ImVec2(floorf(rect_center_x - w * 2.5f), floorf(rect_center_y - w * 2.5f)); + const auto baseBr = ImVec2(floorf(baseTl.x + w), floorf(baseTl.y + w)); + + auto tl = baseTl; + auto br = baseBr; + for (int i = 0; i < 3; ++i) { + tl.x = baseTl.x; + br.x = baseBr.x; + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + if (i != 1 || filled) + drawList->AddRectFilled(tl, br, color); + tl.x += w * 2; + br.x += w * 2; + drawList->AddRectFilled(tl, br, color); + + tl.y += w * 2; + br.y += w * 2; + } + + triangleStart = br.x + w + 1.0f / 24.0f * rect_w; + } + + if (type == IconType::RoundSquare) { + if (filled) { + const auto r = 0.5f * rect_w / 2.0f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + drawList->AddRectFilled(p0, p1, color, cr, 15); + } else { + const auto r = 0.5f * rect_w / 2.0f - 0.5f; + const auto cr = r * 0.5f; + const auto p0 = rect_center - ImVec2(r, r); + const auto p1 = rect_center + ImVec2(r, r); + + if (innerColor & 0xFF000000) + drawList->AddRectFilled(p0, p1, innerColor, cr, 15); + + drawList->AddRect(p0, p1, color, cr, 15, 2.0f * outline_scale); + } + } else if (type == IconType::Diamond) { + if (filled) { + const auto r = 0.607f * rect_w / 2.0f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + drawList->PathFillConvex(color); + } else { + const auto r = 0.607f * rect_w / 2.0f - 0.5f; + const auto c = rect_center; + + drawList->PathLineTo(c + ImVec2(0, -r)); + drawList->PathLineTo(c + ImVec2(r, 0)); + drawList->PathLineTo(c + ImVec2(0, r)); + drawList->PathLineTo(c + ImVec2(-r, 0)); + + if (innerColor & 0xFF000000) + drawList->AddConvexPolyFilled(drawList->_Path.Data, drawList->_Path.Size, innerColor); + + drawList->PathStroke(color, true, 2.0f * outline_scale); + } + } else { + const auto triangleTip = triangleStart + rect_w * (0.45f - 0.32f); + + drawList->AddTriangleFilled( + ImVec2(ceilf(triangleTip), rect_y + rect_h * 0.5f), + ImVec2(triangleStart, rect_center_y + 0.15f * rect_h), + ImVec2(triangleStart, rect_center_y - 0.15f * rect_h), + color); + } + } +} + +void ImGui::Icon(const ImVec2& size, IconType type, bool filled, const ImVec4& color, const ImVec4& innerColor) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/utilities/widgets.cpp + // ax::NodeEditor::Icon + + if (ImGui::IsRectVisible(size)) + { + auto cursorPos = ImGui::GetCursorScreenPos(); + auto drawList = ImGui::GetWindowDrawList(); + DrawIcon(drawList, cursorPos, cursorPos + size, type, filled, ImColor(color), ImColor(innerColor)); + } + + ImGui::Dummy(size); +} + +bool ImGui::Splitter(bool splitVertically, float thickness, float* size1, float* size2, float minSize1, float minSize2, float splitterLongAxisSize) +{ + // Taken from https://github.com/thedmd/imgui-node-editor/blob/master/examples/blueprints-example/blueprints-example.cpp + // ::Splitter + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID("##Splitter"); + ImRect bb; + bb.Min = window->DC.CursorPos + (splitVertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); + bb.Max = bb.Min + CalcItemSize(splitVertically ? ImVec2(thickness, splitterLongAxisSize) : ImVec2(splitterLongAxisSize, thickness), 0.0f, 0.0f); + return SplitterBehavior(bb, id, splitVertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, minSize1, minSize2, 0.0f); +} diff --git a/app/source/Cplt/UI/UI_Workflows.cpp b/app/source/Cplt/UI/UI_Workflows.cpp new file mode 100644 index 0000000..5eea53a --- /dev/null +++ b/app/source/Cplt/UI/UI_Workflows.cpp @@ -0,0 +1,293 @@ +#include "UI.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/Model/Workflow/Nodes/DocumentNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/NumericNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/TextNodes.hpp> +#include <Cplt/Model/Workflow/Nodes/UserInputNodes.hpp> +#include <Cplt/Model/Workflow/Workflow.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_node_editor.h> +#include <imgui_stdlib.h> +#include <memory> +#include <span> +#include <vector> + +namespace ImNodes = ax::NodeEditor; + +namespace CPLT_UNITY_ID { +class WorkflowUI +{ +private: + std::unique_ptr<Workflow> mWorkflow; + + ImNodes::EditorContext* mContext; + + ImNodes::NodeId mContextMenuNodeId = 0; + ImNodes::PinId mContextMenuPinId = 0; + ImNodes::LinkId mContextMenuLinkId = 0; + +public: + WorkflowUI(std::unique_ptr<Workflow> workflow) + : mWorkflow{ std::move(workflow) } + { + mContext = ImNodes::CreateEditor(); + } + + ~WorkflowUI() + { + ImNodes::DestroyEditor(mContext); + } + + void Display() + { + 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("CreateNodeCtxMenu"); + 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("NodeCtxMenu"); + } else if (ImNodes::ShowPinContextMenu(&mContextMenuPinId)) { + ImGui::OpenPopup("PinCtxMenu"); + } else if (ImNodes::ShowLinkContextMenu(&mContextMenuLinkId)) { + ImGui::OpenPopup("LinkCtxMenu"); + } + + if (ImGui::BeginPopup("NodeCtxMenu")) { + auto& node = *mWorkflow->GetNodeByNodeId(mContextMenuNodeId); + node.DrawDebugInfo(); + + if (ImGui::MenuItem(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE))) { + ImNodes::DeleteNode(mContextMenuNodeId); + } + + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("PinCtxMenu")) { + auto [node, pinId, isOutput] = mWorkflow->DisassembleGlobalPinId(mContextMenuPinId); + if (isOutput) { + node->DrawOutputPinDebugInfo(pinId); + } else { + node->DrawInputPinDebugInfo(pinId); + } + + if (ImGui::MenuItem(ICON_FA_UNLINK " " I18N_TEXT("Disconnect", L10N_DISCONNECT))) { + 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("LinkCtxMenu")) { + auto& conn = *mWorkflow->GetConnectionByLinkId(mContextMenuLinkId); + conn.DrawDebugInfo(); + + if (ImGui::MenuItem(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE))) { + ImNodes::DeleteLink(mContextMenuLinkId); + } + + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("CreateNodeCtxMenu")) { + for (int i = WorkflowNode::CG_Numeric; i < WorkflowNode::InvalidCategory; ++i) { + auto category = (WorkflowNode::Category)i; + auto members = WorkflowNode::QueryCategoryMembers(category); + + if (ImGui::BeginMenu(WorkflowNode::FormatCategory(category))) { + for (auto member : members) { + if (ImGui::MenuItem(WorkflowNode::FormatKind(member))) { + // Create node + auto uptr = WorkflowNode::CreateByKind(member); + mWorkflow->AddNode(std::move(uptr)); + } + } + ImGui::EndMenu(); + } + } + ImGui::EndPopup(); + } + + if (tooltipMessage) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(tooltipMessage); + ImGui::EndTooltip(); + } + ImNodes::Resume(); + + ImNodes::End(); + } + + void Close() + { + // TODO + } +}; +} // namespace CPLT_UNITY_ID + +void UI::WorkflowsTab() +{ + auto& project = *GlobalStates::GetInstance().GetCurrentProject(); + + static std::unique_ptr<CPLT_UNITY_ID::WorkflowUI> openWorkflow; + static AssetList::ListState state; + bool openedDummy = true; + + // Toolbar item: close + if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE), openWorkflow == nullptr)) { + openWorkflow->Close(); + openWorkflow = nullptr; + } + + // Toolbar item: open... + ImGui::SameLine(); + if (ImGui::Button((I18N_TEXT("Open asset...", L10N_ASSET_OPEN)))) { + ImGui::OpenPopup(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(ICON_FA_FOLDER_OPEN " " I18N_TEXT("Open", L10N_OPEN), state.SelectedAsset == nullptr)) { + ImGui::CloseCurrentPopup(); + + auto workflow = project.Workflows.Load(*state.SelectedAsset); + openWorkflow = std::make_unique<CPLT_UNITY_ID::WorkflowUI>(std::move(workflow)); + } + ImGui::SameLine(); + project.Workflows.DisplayControls(state); + project.Workflows.DisplayDetailsList(state); + + ImGui::EndPopup(); + } + + // Toolbar item: manage... + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Manage assets...", L10N_ASSET_MANAGE))) { + ImGui::OpenPopup(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + project.Workflows.DisplayControls(state); + project.Workflows.DisplayDetailsList(state); + ImGui::EndPopup(); + } + + if (openWorkflow) { + openWorkflow->Display(); + } +} diff --git a/app/source/Cplt/UI/fwd.hpp b/app/source/Cplt/UI/fwd.hpp new file mode 100644 index 0000000..756e567 --- /dev/null +++ b/app/source/Cplt/UI/fwd.hpp @@ -0,0 +1,6 @@ +#pragma once + +// UI.hpp +namespace ImGui { +enum class IconType; +} |