From 7fe47a9d5b1727a61dc724523b530762f6d6ba19 Mon Sep 17 00:00:00 2001 From: rtk0c Date: Thu, 30 Jun 2022 21:38:53 -0700 Subject: Restructure project --- app/source/Cplt/UI/UI.hpp | 48 ++ app/source/Cplt/UI/UI_DatabaseView.cpp | 668 ++++++++++++++++++++++ app/source/Cplt/UI/UI_Items.cpp | 252 +++++++++ app/source/Cplt/UI/UI_MainWindow.cpp | 237 ++++++++ app/source/Cplt/UI/UI_Settings.cpp | 8 + app/source/Cplt/UI/UI_Templates.cpp | 977 +++++++++++++++++++++++++++++++++ app/source/Cplt/UI/UI_Utils.cpp | 315 +++++++++++ app/source/Cplt/UI/UI_Workflows.cpp | 293 ++++++++++ app/source/Cplt/UI/fwd.hpp | 6 + 9 files changed, 2804 insertions(+) create mode 100644 app/source/Cplt/UI/UI.hpp create mode 100644 app/source/Cplt/UI/UI_DatabaseView.cpp create mode 100644 app/source/Cplt/UI/UI_Items.cpp create mode 100644 app/source/Cplt/UI/UI_MainWindow.cpp create mode 100644 app/source/Cplt/UI/UI_Settings.cpp create mode 100644 app/source/Cplt/UI/UI_Templates.cpp create mode 100644 app/source/Cplt/UI/UI_Utils.cpp create mode 100644 app/source/Cplt/UI/UI_Workflows.cpp create mode 100644 app/source/Cplt/UI/fwd.hpp (limited to 'app/source/Cplt/UI') 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 + +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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CPLT_UNITY_ID { + +// TODO move to Settings +constexpr int kMaxEntriesPerPage = 32; +constexpr int kSummaryItemCount = 3; +constexpr int kSummaryMaxLength = 25; + +std::pair 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 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 AssociatedDeliveries; + std::vector 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 AssociatedDeliveries; + std::vector Items; + std::string ItemsSummary; + std::string Factory; + std::string OrderTime; + std::string DeliveryTime; + int Id; + bool DeliveriesCached; +}; + +template +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; + + 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 mActiveFilter; + + tsl::robin_map mPages; + + /// A vector of entry indices (in \c mEntries) that are visible under the current filter. + std::vector 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 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 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 LoadItems(SQLite::Statement& stmt, int64_t id) + { + // clang-format off + DEFER { stmt.reset(); }; + // clang-format on + + stmt.bind(1, id); + + std::vector 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& items) + { + if (items.empty()) { + return ""; + } + + 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 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 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& 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 +{ +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::OnProjectChanged(newProject); + } +#pragma clang diagnostic pop +}; + +class PurchasesTableView : public GenericTableView +{ +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::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 +#include +#include + +#include +#include +#include + +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 +ActionResult ItemEditor(ItemList& 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 +void ItemListEntries(ItemList& 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 +void ItemListEditor(ItemList& 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(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 +#include +#include + +#include +#include +#include +#include +#include +#include + +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(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(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(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 + +#include + +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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CPLT_UNITY_ID { +class TemplateUI +{ +public: + static std::unique_ptr CreateByKind(std::unique_ptr