diff options
Diffstat (limited to 'app/source/Cplt/Model')
49 files changed, 5726 insertions, 0 deletions
diff --git a/app/source/Cplt/Model/Assets.cpp b/app/source/Cplt/Model/Assets.cpp new file mode 100644 index 0000000..0dfe847 --- /dev/null +++ b/app/source/Cplt/Model/Assets.cpp @@ -0,0 +1,306 @@ +#include "Assets.hpp" + +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/DataStream.hpp> +#include <Cplt/Utils/IO/StringIntegration.hpp> +#include <Cplt/Utils/IO/UuidIntegration.hpp> + +#include <IconsFontAwesome.h> +#include <imgui.h> +#include <imgui_stdlib.h> +#include <tsl/array_map.h> +#include <string> +#include <utility> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; + +template <class TSavedAsset, class TStream> +void OperateStreamForSavedAsset(TSavedAsset& cell, TStream& proxy) +{ + proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Name); + proxy.template ObjectAdapted<DataStreamAdapters::Uuid>(cell.Uuid); + proxy.Value(cell.Payload); +} + +void SavedAsset::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForSavedAsset(*this, stream); +} + +void SavedAsset::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForSavedAsset(*this, stream); +} + +Asset::Asset() = default; + +class AssetList::Private +{ +public: + Project* ConnectedProject; + tsl::array_map<char, SavedAsset> Assets; + tsl::array_map<char, std::unique_ptr<Asset>> Cache; + int CacheSizeLimit = 0; + + struct + { + std::string NewName; + NameSelectionError NewNameError = NameSelectionError::Empty; + + void ShowErrors() const + { + switch (NewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + } + + bool HasErrors() const + { + return NewNameError != NameSelectionError::None; + } + + void Validate(const AssetList& self) + { + if (NewName.empty()) { + NewNameError = NameSelectionError::Empty; + return; + } + + if (self.FindByName(NewName)) { + NewNameError = NameSelectionError::Duplicated; + return; + } + + NewNameError = NameSelectionError::None; + } + } PopupPrivateState; +}; + +AssetList::AssetList(Project& project) + : mPrivate{ std::make_unique<Private>() } +{ + mPrivate->ConnectedProject = &project; +} + +// Write an empty destructor here so std::unique_ptr's destructor can see AssetList::Private's implementation +AssetList::~AssetList() +{ +} + +Project& AssetList::GetConnectedProject() const +{ + return *mPrivate->ConnectedProject; +} + +void AssetList::Reload() +{ + // TODO fix asset dicovery loading + mPrivate->Assets.clear(); + mPrivate->Cache.clear(); + DiscoverFiles([this](SavedAsset asset) -> void { + mPrivate->Assets.insert(asset.Name, std::move(asset)); + }); +} + +int AssetList::GetCount() const +{ + return mPrivate->Assets.size(); +} + +const tsl::array_map<char, SavedAsset>& AssetList::GetAssets() const +{ + return mPrivate->Assets; +} + +const SavedAsset* AssetList::FindByName(std::string_view name) const +{ + auto iter = mPrivate->Assets.find(name); + if (iter != mPrivate->Assets.end()) { + return &iter.value(); + } else { + return nullptr; + } +} + +const SavedAsset& AssetList::Create(SavedAsset asset) +{ + auto [iter, DISCARD] = mPrivate->Assets.insert(asset.Name, SavedAsset{}); + auto& savedAsset = iter.value(); + + savedAsset = std::move(asset); + if (savedAsset.Uuid.is_nil()) { + savedAsset.Uuid = uuids::uuid_random_generator{}(); + } + + SaveInstance(savedAsset, nullptr); + + return savedAsset; +} + +std::unique_ptr<Asset> AssetList::CreateAndLoad(SavedAsset assetIn) +{ + auto& savedAsset = Create(std::move(assetIn)); + auto asset = std::unique_ptr<Asset>(CreateInstance(savedAsset)); + return asset; +} + +std::unique_ptr<Asset> AssetList::Load(std::string_view name) const +{ + if (auto savedAsset = FindByName(name)) { + auto asset = Load(*savedAsset); + return asset; + } else { + return nullptr; + } +} + +std::unique_ptr<Asset> AssetList::Load(const SavedAsset& asset) const +{ + return std::unique_ptr<Asset>(LoadInstance(asset)); +} + +const SavedAsset* AssetList::Rename(std::string_view oldName, std::string_view newName) +{ + auto iter = mPrivate->Assets.find(oldName); + if (iter == mPrivate->Assets.end()) return nullptr; + + auto info = std::move(iter.value()); + info.Name = newName; + + RenameInstanceOnDisk(info, oldName); + + mPrivate->Assets.erase(iter); + auto [newIter, DISCARD] = mPrivate->Assets.insert(newName, std::move(info)); + + return &newIter.value(); +} + +bool AssetList::Remove(std::string_view name) +{ + auto iter = mPrivate->Assets.find(name); + if (iter == mPrivate->Assets.end()) { + return false; + } + auto& asset = iter.value(); + + fs::remove(RetrievePathFromAsset(asset)); + mPrivate->Assets.erase(iter); + + return true; +} + +int AssetList::GetCacheSizeLimit() const +{ + return mPrivate->CacheSizeLimit; +} + +void AssetList::SetCacheSizeLimit(int limit) +{ + mPrivate->CacheSizeLimit = limit; +} + +void AssetList::DisplayIconsList(ListState& state) +{ + // TODO +} + +void AssetList::DisplayDetailsList(ListState& state) +{ + // Note: stub function remained here in case any state processing needs to be done before issuing to implementers + DisplayDetailsTable(state); +} + +void AssetList::DisplayControls(ListState& state) +{ + auto& ps = mPrivate->PopupPrivateState; + bool openedDummy = true; + + if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) { + ImGui::OpenPopup(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + DisplayAssetCreator(state); + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_I_CURSOR " " I18N_TEXT("Rename", L10N_RENAME), state.SelectedAsset == nullptr)) { + ImGui::OpenPopup(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &ps.NewName)) { + ps.Validate(*this); + } + + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), ps.HasErrors())) { + ImGui::CloseCurrentPopup(); + + auto movedAsset = Rename(state.SelectedAsset->Name, ps.NewName); + // Update the selected pointer to the new location (we mutated the map, the pointer may be invalid now) + state.SelectedAsset = movedAsset; + + ps = {}; + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + + ps.ShowErrors(); + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), state.SelectedAsset == nullptr)) { + ImGui::OpenPopup(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE)); + } + if (ImGui::BeginPopupModal(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) { + ImGui::CloseCurrentPopup(); + + auto& assetName = state.SelectedAsset->Name; + Remove(assetName); + + state.SelectedAsset = nullptr; + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void AssetList::DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, std::string_view extension) const +{ + for (auto entry : fs::directory_iterator(containerDir)) { + if (!entry.is_regular_file()) continue; + + // If the caller provided an extension to match against, and it doesn't equal to current file extension, skip + if (!extension.empty() && + entry.path().extension() != extension) + { + continue; + } + + callback(SavedAsset{ + .Name = RetrieveNameFromFile(entry.path()), + .Uuid = RetrieveUuidFromFile(entry.path()), + // TODO load payload + }); + } +} + +void AssetList::DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, const std::function<bool(std::istream&)>& validater) const +{ + // TODO +} diff --git a/app/source/Cplt/Model/Assets.hpp b/app/source/Cplt/Model/Assets.hpp new file mode 100644 index 0000000..d2f8570 --- /dev/null +++ b/app/source/Cplt/Model/Assets.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include <Cplt/Utils/UUID.hpp> +#include <Cplt/fwd.hpp> + +#include <tsl/array_map.h> +#include <filesystem> +#include <iosfwd> +#include <memory> +#include <string_view> + +/// A structure representing a ready-to-be-loaded asset, locating on the disk. +/// Each asset should be identified by a unique uuid within the asset category (i.e. a workflow and a template can share the same uuid), +/// generated on insertion to an asset list if not given by the caller. +struct SavedAsset +{ + std::string Name; + /// UUID of this asset. This field is generated as a random UUID v4 upon insertion into an AssetList, if not already provided by the caller (indicated by !is_nil()). + uuids::uuid Uuid; + /// Extra data to be used by the AssetList/Asset implementation. + uint64_t Payload; + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +class Asset +{ +public: + Asset(); + virtual ~Asset() = default; +}; + +enum class NameSelectionError +{ + None, + Duplicated, + Empty, +}; + +class AssetList +{ +private: + class Private; + std::unique_ptr<Private> mPrivate; + +public: + AssetList(Project& project); + virtual ~AssetList(); + + Project& GetConnectedProject() const; + + // TODO support file watches + void Reload(); + + int GetCount() const; + // TODO convert to custom iterable + const tsl::array_map<char, SavedAsset>& GetAssets() const; + + const SavedAsset* FindByName(std::string_view name) const; + const SavedAsset& Create(SavedAsset asset); + std::unique_ptr<Asset> CreateAndLoad(SavedAsset asset); + /// Load the asset on disk by its name. + std::unique_ptr<Asset> Load(std::string_view name) const; + /// Load the asset on disk by a reference to its SavedAsset instance. This function assumes that the given SavedAsset + /// is stored in AssetList, otherwise the behavior is undefined. + std::unique_ptr<Asset> Load(const SavedAsset& asset) const; + const SavedAsset* Rename(std::string_view oldName, std::string_view newName); + bool Remove(std::string_view name); + + int GetCacheSizeLimit() const; + void SetCacheSizeLimit(int limit); + + struct ListState + { + const SavedAsset* SelectedAsset = nullptr; + }; + void DisplayIconsList(ListState& state); + void DisplayDetailsList(ListState& state); + void DisplayControls(ListState& state); + +protected: + virtual void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const = 0; + + // Helper + void DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, std::string_view extension) const; + void DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, const std::function<bool(std::istream&)>& validater) const; + + /// Create an empty/default instance of this asset type on disk, potentially qualified by SavedAsset::Payload. + /// Return `true` on success and `false` on failure. + virtual bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const = 0; + /// The returned pointer indicate ownership to the object. + virtual Asset* LoadInstance(const SavedAsset& assetInfo) const = 0; + /// Create an empty/default instance of this asset type, potentially qualified by SavedAsset::Payload. + /// The returned pointer indicate ownership to the object. + virtual Asset* CreateInstance(const SavedAsset& assetInfo) const = 0; + virtual bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const = 0; + + virtual std::string RetrieveNameFromFile(const std::filesystem::path& file) const = 0; + virtual uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const = 0; + virtual std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const = 0; + + virtual void DisplayAssetCreator(ListState& state) = 0; + virtual void DisplayDetailsTable(ListState& state) const = 0; +}; + +template <class T> +class AssetListTyped : public AssetList +{ +public: + using AssetList::AssetList; + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "HidingNonVirtualFunction" + std::unique_ptr<T> CreateAndLoad(SavedAsset asset) + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::CreateAndLoad(asset).release())); + } + + std::unique_ptr<T> Load(std::string_view name) const + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(name).release())); + } + + std::unique_ptr<T> Load(const SavedAsset& asset) const + { + return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(asset).release())); + } +#pragma clang diagnostic pop +}; diff --git a/app/source/Cplt/Model/Database.cpp b/app/source/Cplt/Model/Database.cpp new file mode 100644 index 0000000..07c6e36 --- /dev/null +++ b/app/source/Cplt/Model/Database.cpp @@ -0,0 +1,163 @@ +#include "Database.hpp" + +#include <Cplt/Model/Project.hpp> + +#include <filesystem> +#include <stdexcept> + +namespace fs = std::filesystem; + +SalesTable::SalesTable(MainDatabase& db) + // language=SQLite + : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Sales") + // language=SQLite + , GetRows(db.GetSQLite(), "SELECT * FROM Sales LIMIT ? OFFSET ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM SalesItems WHERE SaleId == ?") +{ +} + +PurchasesTable::PurchasesTable(MainDatabase& db) + // language=SQLite + : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Purchases") + // language=SQLite + , GetRows(db.GetSQLite(), "SELECT * FROM Purchases LIMIT ? OFFSET ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM PurchasesItems WHERE PurchaseId == ?") +{ +} + +DeliveryTable::DeliveryTable(MainDatabase& db) + // language=SQLite + : FilterByTypeAndId(db.GetSQLite(), "SELECT * FROM Deliveries WHERE AssociatedOrder == ? AND Outgoing = ?") + // language=SQLite + , GetItems(db.GetSQLite(), "SELECT * FROM DeliveriesItems WHERE DeliveryId == ?") +{ +} + +static std::string GetDatabaseFilePath(const Project& project) +{ + auto dbsDir = project.GetPath() / "databases"; + fs::create_directories(dbsDir); + + auto dbFile = dbsDir / "transactions.sqlite3"; + return dbFile.string(); +} + +/// Wrapper for SQLite::Database that creates the default tables +MainDatabase::DatabaseWrapper::DatabaseWrapper(MainDatabase& self) + : mSqlite(GetDatabaseFilePath(*self.mProject), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE) +{ + // If this table doesn't exist, the database probably just got initialized + if (mSqlite.tableExists("Sales")) { + return; + } + + // 'Sales' schema + // - Customer: the customer item ID + // - Deadline: unix epoch time of order deadline + // - DeliveryTime: the time this order was completed (through a set of deliveries) + + // 'Purchases' schema + // - Factory: the factory id, + // - OrderTime: the time this order was made + // - DeliveryTime: the time this order was completed (through a set of deliveries) + + // 'Deliveries' schema + // - ShipmentTime: unix epoch time stamp of sending to delivery + // - ArrivalTime: unix epoch time stamp of delivery arrived at warehouse; 0 if not arrived yet + // - AssociatedOrder: Id of the order that this delivery is completing (which table: Outgoing=true -> Sales, Outgoing=false -> Purchases) + // - Outgoing: true if the delivery is from warehouse to customer; false if the delivery is from factory to warehouse + + // Note: the 'Id' key would be unique (not recycled after row deletion) because it's explicit + // https://www.sqlite.org/rowidtable.html + + // language=SQLite + mSqlite.exec(R"""( +CREATE TABLE IF NOT EXISTS Sales( + Id INT PRIMARY KEY, + Customer INT, + Deadline DATETIME, + DeliveryTime DATETIME +); +CREATE TABLE IF NOT EXISTS SalesItems( + SaleId INT, + ItemId INT, + Count INT +); + +CREATE TABLE IF NOT EXISTS Purchases( + Id INT PRIMARY KEY, + Factory INT, + OrderTime DATETIME, + DeliveryTime DATETIME +); +CREATE TABLE IF NOT EXISTS PurchasesItems( + PurchaseId INT, + ItemId INT, + Count INT +); + +CREATE TABLE IF NOT EXISTS Deliveries( + Id INT PRIMARY KEY, + ShipmentTime DATETIME, + ArrivalTime DATETIME, + AssociatedOrder INT, + Outgoing BOOLEAN +); +CREATE TABLE IF NOT EXISTS DeliveriesItems( + DeliveryId INT, + ItemId INT, + Count INT +); +)"""); +} + +MainDatabase::MainDatabase(Project& project) + : mProject{ &project } + , mDbWrapper(*this) + , mSales(*this) + , mPurchases(*this) + , mDeliveries(*this) +{ +} + +const SQLite::Database& MainDatabase::GetSQLite() const +{ + return mDbWrapper.mSqlite; +} + +SQLite::Database& MainDatabase::GetSQLite() +{ + return mDbWrapper.mSqlite; +} + +const SalesTable& MainDatabase::GetSales() const +{ + return mSales; +} + +SalesTable& MainDatabase::GetSales() +{ + return mSales; +} + +const PurchasesTable& MainDatabase::GetPurchases() const +{ + return mPurchases; +} + +PurchasesTable& MainDatabase::GetPurchases() +{ + return mPurchases; +} + +const DeliveryTable& MainDatabase::GetDeliveries() const +{ + return mDeliveries; +} + +DeliveryTable& MainDatabase::GetDeliveries() +{ + return mDeliveries; +} diff --git a/app/source/Cplt/Model/Database.hpp b/app/source/Cplt/Model/Database.hpp new file mode 100644 index 0000000..222e43d --- /dev/null +++ b/app/source/Cplt/Model/Database.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include <Cplt/fwd.hpp> + +#include <SQLiteCpp/Database.h> +#include <SQLiteCpp/Statement.h> +#include <cstdint> + +enum class TableKind +{ + Sales, + SalesItems, + Purchases, + PurchasesItems, + Deliveries, + DeliveriesItems, +}; + +class SalesTable +{ +public: + SQLite::Statement GetRowCount; + SQLite::Statement GetRows; + SQLite::Statement GetItems; + +public: + SalesTable(MainDatabase& db); +}; + +class PurchasesTable +{ +public: + SQLite::Statement GetRowCount; + SQLite::Statement GetRows; + SQLite::Statement GetItems; + +public: + PurchasesTable(MainDatabase& db); +}; + +class DeliveryTable +{ +public: + SQLite::Statement FilterByTypeAndId; + SQLite::Statement GetItems; + +public: + DeliveryTable(MainDatabase& db); +}; + +class MainDatabase +{ +private: + class DatabaseWrapper + { + public: + SQLite::Database mSqlite; + DatabaseWrapper(MainDatabase& self); + }; + + Project* mProject; + DatabaseWrapper mDbWrapper; + SalesTable mSales; + PurchasesTable mPurchases; + DeliveryTable mDeliveries; + +public: + MainDatabase(Project& project); + + const SQLite::Database& GetSQLite() const; + SQLite::Database& GetSQLite(); + + const SalesTable& GetSales() const; + SalesTable& GetSales(); + const PurchasesTable& GetPurchases() const; + PurchasesTable& GetPurchases(); + const DeliveryTable& GetDeliveries() const; + DeliveryTable& GetDeliveries(); +}; diff --git a/app/source/Cplt/Model/Filter.cpp b/app/source/Cplt/Model/Filter.cpp new file mode 100644 index 0000000..1e4b31b --- /dev/null +++ b/app/source/Cplt/Model/Filter.cpp @@ -0,0 +1 @@ +#include "Filter.hpp" diff --git a/app/source/Cplt/Model/Filter.hpp b/app/source/Cplt/Model/Filter.hpp new file mode 100644 index 0000000..1b923e1 --- /dev/null +++ b/app/source/Cplt/Model/Filter.hpp @@ -0,0 +1,6 @@ +#pragma once + +class TableRowsFilter +{ + // TODO +}; diff --git a/app/source/Cplt/Model/GlobalStates.cpp b/app/source/Cplt/Model/GlobalStates.cpp new file mode 100644 index 0000000..417514f --- /dev/null +++ b/app/source/Cplt/Model/GlobalStates.cpp @@ -0,0 +1,163 @@ +#include "GlobalStates.hpp" + +#include <Cplt/Model/Project.hpp> +#include <Cplt/Utils/StandardDirectories.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <cassert> +#include <filesystem> +#include <fstream> +#include <memory> + +namespace fs = std::filesystem; + +static std::unique_ptr<GlobalStates> globalStateInstance; +static fs::path globalDataPath; + +void GlobalStates::Init() +{ + Init(StandardDirectories::UserData() / "cplt"); +} + +void GlobalStates::Init(std::filesystem::path userDataDir) +{ + globalStateInstance = std::make_unique<GlobalStates>(); + globalDataPath = userDataDir; + fs::create_directories(globalDataPath); + + // Reading recent projects + { + std::ifstream ifs(globalDataPath / "recents.json"); + if (!ifs) return; + + Json::Value root; + ifs >> root; + + if (!root.isObject()) return; + if (auto& recents = root["RecentProjects"]; recents.isArray()) { + for (auto& elm : recents) { + if (!elm.isString()) continue; + + fs::path path(elm.asCString()); + if (!fs::exists(path)) continue; + + auto utf8String = path.string(); + globalStateInstance->mRecentProjects.push_back(RecentProject{ + .Path = std::move(path), + .CachedUtf8String = std::move(utf8String), + }); + } + } + } +} + +void GlobalStates::Shutdown() +{ + if (!globalStateInstance) return; + + globalStateInstance->SetCurrentProject(nullptr); + + if (globalStateInstance->mDirty) { + globalStateInstance->WriteToDisk(); + } +} + +GlobalStates& GlobalStates::GetInstance() +{ + return *globalStateInstance; +} + +const std::filesystem::path& GlobalStates::GetUserDataPath() +{ + return globalDataPath; +} + +const std::vector<GlobalStates::RecentProject>& GlobalStates::GetRecentProjects() const +{ + return mRecentProjects; +} + +void GlobalStates::ClearRecentProjects() +{ + mRecentProjects.clear(); + MarkDirty(); +} + +void GlobalStates::AddRecentProject(const Project& project) +{ + mRecentProjects.push_back(RecentProject{ + .Path = project.GetPath(), + .CachedUtf8String = project.GetPath().string(), + }); + MarkDirty(); +} + +void GlobalStates::MoveProjectToTop(const Project& project) +{ + for (auto it = mRecentProjects.begin(); it != mRecentProjects.end(); ++it) { + if (it->Path == project.GetPath()) { + std::rotate(it, it + 1, mRecentProjects.end()); + MarkDirty(); + return; + } + } + AddRecentProject(project); +} + +void GlobalStates::RemoveRecentProject(int idx) +{ + assert(idx >= 0 && idx < mRecentProjects.size()); + + mRecentProjects.erase(mRecentProjects.begin() + idx); + MarkDirty(); +} + +bool GlobalStates::HasCurrentProject() const +{ + return mCurrentProject != nullptr; +} + +Project* GlobalStates::GetCurrentProject() const +{ + return mCurrentProject.get(); +} + +void GlobalStates::SetCurrentProject(std::unique_ptr<Project> project) +{ + if (mCurrentProject) { + mCurrentProject->WriteToDisk(); + mCurrentProject = nullptr; + } + if (project) { + MoveProjectToTop(*project); + } + mCurrentProject = std::move(project); +} + +void GlobalStates::WriteToDisk() const +{ + Json::Value root; + + auto& recentProjects = root["RecentProjects"] = Json::Value(Json::arrayValue); + for (auto& [path, _] : mRecentProjects) { + recentProjects.append(Json::Value(path.string())); + } + + std::ofstream ofs(globalDataPath / "recents.json"); + ofs << root; + + mDirty = false; +} + +bool GlobalStates::IsDirty() const +{ + return mDirty; +} + +void GlobalStates::MarkDirty() +{ + mDirty = true; + OnModified(); +} diff --git a/app/source/Cplt/Model/GlobalStates.hpp b/app/source/Cplt/Model/GlobalStates.hpp new file mode 100644 index 0000000..1eb47fb --- /dev/null +++ b/app/source/Cplt/Model/GlobalStates.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include <Cplt/Utils/Sigslot.hpp> +#include <Cplt/fwd.hpp> + +#include <filesystem> +#include <string> +#include <vector> + +class GlobalStates +{ +public: + static void Init(); + static void Init(std::filesystem::path userDataDir); + static void Shutdown(); + + static GlobalStates& GetInstance(); + static const std::filesystem::path& GetUserDataPath(); + + struct RecentProject + { + std::filesystem::path Path; + std::string CachedUtf8String; + }; + +public: + Signal<> OnModified; + +private: + std::vector<RecentProject> mRecentProjects; + std::unique_ptr<Project> mCurrentProject; + mutable bool mDirty = false; + +public: + const std::vector<RecentProject>& GetRecentProjects() const; + void ClearRecentProjects(); + void AddRecentProject(const Project& project); + /// Move or add the project to end of the recent projects list. + /// If the project is not in the list of recently used projects, it will be appended, otherwise + /// it will be moved to the end. + void MoveProjectToTop(const Project& project); + void RemoveRecentProject(int idx); + + bool HasCurrentProject() const; + Project* GetCurrentProject() const; + void SetCurrentProject(std::unique_ptr<Project> project); + + // TODO async autosaving to prevent data loss on crash + void WriteToDisk() const; + + bool IsDirty() const; + +private: + void MarkDirty(); +}; diff --git a/app/source/Cplt/Model/Items.cpp b/app/source/Cplt/Model/Items.cpp new file mode 100644 index 0000000..9d2abc6 --- /dev/null +++ b/app/source/Cplt/Model/Items.cpp @@ -0,0 +1,114 @@ +#include "Items.hpp" + +const std::string& ProductItem::GetDescription() const +{ + return mDescription; +} + +void ProductItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +int ProductItem::GetPrice() const +{ + return mPrice; +} +void ProductItem::SetPrice(int price) +{ + mPrice = price; +} + +int ProductItem::GetStock() const +{ + return mStock; +} + +void ProductItem::SetStock(int stock) +{ + mStock = stock; +} + +Json::Value ProductItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Price"] = mPrice; + elm["Stock"] = mStock; + return elm; +} + +void ProductItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mPrice = elm["Price"].asInt(); + mStock = elm["Stock"].asInt(); +} + +const std::string& FactoryItem::GetDescription() const +{ + return mDescription; +} + +void FactoryItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +const std::string& FactoryItem::GetEmail() const +{ + return mEmail; +} + +void FactoryItem::SetEmail(std::string email) +{ + mEmail = std::move(email); +} + +Json::Value FactoryItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Email"] = mEmail; + return elm; +} + +void FactoryItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mEmail = elm["Email"].asString(); +} + +const std::string& CustomerItem::GetDescription() const +{ + return mDescription; +} + +void CustomerItem::SetDescription(std::string description) +{ + mDescription = std::move(description); +} + +const std::string& CustomerItem::GetEmail() const +{ + return mEmail; +} + +void CustomerItem::SetEmail(std::string email) +{ + mEmail = std::move(email); +} + +Json::Value CustomerItem::Serialize() const +{ + Json::Value elm; + elm["Description"] = mDescription; + elm["Email"] = mEmail; + return elm; +} + +void CustomerItem::Deserialize(const Json::Value& elm) +{ + mDescription = elm["Description"].asString(); + mEmail = elm["Email"].asString(); +} diff --git a/app/source/Cplt/Model/Items.hpp b/app/source/Cplt/Model/Items.hpp new file mode 100644 index 0000000..c00ee59 --- /dev/null +++ b/app/source/Cplt/Model/Items.hpp @@ -0,0 +1,253 @@ +#pragma once + +#include <Cplt/fwd.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <tsl/array_map.h> +#include <cstddef> +#include <limits> +#include <stdexcept> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +template <class T> +class ItemList +{ +private: + std::vector<T> mStorage; + tsl::array_map<char, size_t> mNameLookup; + +public: + template <class... Args> + T& Insert(std::string name, Args... args) + { + auto iter = mNameLookup.find(name); + if (iter != mNameLookup.end()) { + throw std::runtime_error("Duplicate key."); + } + + for (size_t i = 0; i < mStorage.size(); ++i) { + if (mStorage[i].IsInvalid()) { + mStorage[i] = T(*this, i, std::move(name), std::forward<Args>(args)...); + mNameLookup.insert(name, i); + return mStorage[i]; + } + } + + size_t id = mStorage.size(); + mNameLookup.insert(name, id); + mStorage.emplace_back(*this, id, std::move(name), std::forward<Args>(args)...); + return mStorage[id]; + } + + void Remove(size_t index) + { + auto& item = mStorage[index]; + mNameLookup.erase(item.GetName()); + mStorage[index] = T(*this); + } + + T* Find(size_t id) + { + return &mStorage[id]; + } + + const T* Find(size_t id) const + { + return &mStorage[id]; + } + + const T* Find(std::string_view name) const + { + auto iter = mNameLookup.find(name); + if (iter != mNameLookup.end()) { + return &mStorage[iter.value()]; + } else { + return nullptr; + } + } + + Json::Value Serialize() const + { + Json::Value items(Json::arrayValue); + for (auto& item : mStorage) { + if (!item.IsInvalid()) { + auto elm = item.Serialize(); + elm["Id"] = item.GetId(); + elm["Name"] = item.GetName(); + items.append(elm); + } + } + + Json::Value root; + root["MaxItemId"] = mStorage.size(); + root["Items"] = std::move(items); + + return root; + } + + ItemList() = default; + + ItemList(const Json::Value& root) + { + constexpr const char* kMessage = "Failed to load item list from JSON."; + + auto& itemCount = root["MaxItemId"]; + if (!itemCount.isIntegral()) throw std::runtime_error(kMessage); + + mStorage.resize(itemCount.asInt64(), T(*this)); + + auto& items = root["Items"]; + if (!items.isArray()) throw std::runtime_error(kMessage); + + for (auto& elm : items) { + if (!elm.isObject()) throw std::runtime_error(kMessage); + + auto& id = elm["Id"]; + if (!id.isIntegral()) throw std::runtime_error(kMessage); + auto& name = elm["Name"]; + if (!name.isString()) throw std::runtime_error(kMessage); + + size_t iid = id.asInt64(); + mStorage[iid] = T(*this, iid, name.asString()); + mStorage[iid].Deserialize(elm); + } + } + + typename decltype(mStorage)::iterator begin() + { + return mStorage.begin(); + } + + typename decltype(mStorage)::iterator end() + { + return mStorage.end(); + } + + typename decltype(mStorage)::const_iterator begin() const + { + return mStorage.begin(); + } + + typename decltype(mStorage)::const_iterator end() const + { + return mStorage.end(); + } + +private: + template <class TSelf> + friend class ItemBase; + + void UpdateItemName(const T& item, const std::string& newName) + { + mNameLookup.erase(item.GetName()); + mNameLookup.insert(newName, item.GetId()); + } +}; + +template <class TSelf> +class ItemBase +{ +private: + ItemList<TSelf>* mList; + size_t mId; + std::string mName; + +public: + ItemBase(ItemList<TSelf>& list, size_t id = std::numeric_limits<size_t>::max(), std::string name = "") + : mList{ &list } + , mId{ id } + , mName{ std::move(name) } + { + } + + bool IsInvalid() const + { + return mId == std::numeric_limits<size_t>::max(); + } + + ItemList<TSelf>& GetList() const + { + return *mList; + } + + size_t GetId() const + { + return mId; + } + + const std::string& GetName() const + { + return mName; + } + + void SetName(std::string name) + { + mList->UpdateItemName(static_cast<TSelf&>(*this), name); + mName = std::move(name); + } +}; + +class ProductItem : public ItemBase<ProductItem> +{ +private: + std::string mDescription; + int mPrice = 0; + int mStock = 0; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + /// Get the price of this item in US cents. + int GetPrice() const; + void SetPrice(int price); + /// Get the current number of this product in warehouse. + /// This is a housekeeping field and shouldn't be editable by the user from the UI. + int GetStock() const; + void SetStock(int stock); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; + +class FactoryItem : public ItemBase<FactoryItem> +{ +private: + std::string mDescription; + std::string mEmail; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + const std::string& GetEmail() const; + void SetEmail(std::string email); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; + +class CustomerItem : public ItemBase<CustomerItem> +{ +private: + std::string mDescription; + std::string mEmail; + +public: + using ItemBase::ItemBase; + + const std::string& GetDescription() const; + void SetDescription(std::string description); + const std::string& GetEmail() const; + void SetEmail(std::string email); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); +}; diff --git a/app/source/Cplt/Model/Project.cpp b/app/source/Cplt/Model/Project.cpp new file mode 100644 index 0000000..a1e9bab --- /dev/null +++ b/app/source/Cplt/Model/Project.cpp @@ -0,0 +1,168 @@ +#include "Project.hpp" + +#include <Cplt/Model/Workflow/Workflow.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <filesystem> +#include <fstream> +#include <stdexcept> +#include <utility> + +namespace fs = std::filesystem; + +template <class T> +static void ReadItemList(ItemList<T>& list, const fs::path& filePath) +{ + std::ifstream ifs(filePath); + if (ifs) { + Json::Value root; + ifs >> root; + + list = ItemList<T>(root); + } +} + +static void CreateProjectSubfolders(const Project& project) +{ + fs::create_directory(project.GetDatabasesDirectory()); + fs::create_directory(project.GetItemsDirectory()); + fs::create_directory(project.GetWorkflowsDirectory()); + fs::create_directory(project.GetTemplatesDirectory()); +} + +Project::Project(fs::path rootPath) + : mRootPath{ std::move(rootPath) } + , mRootPathString{ mRootPath.string() } + , Workflows(*this) + , Templates(*this) + , Database(*this) +{ + // TODO better diagnostic + const char* kInvalidFormatErr = "Failed to load project: invalid format."; + + std::ifstream ifs(mRootPath / "cplt_project.json"); + if (!ifs) { + std::string message; + message += "Failed to load project file at '"; + message += mRootPath.string(); + message += "'."; + throw std::runtime_error(message); + } + + { + Json::Value root; + ifs >> root; + + const auto& croot = root; // Use const reference so that accessors default to returning a null if not found, instead of silently creating new elements + if (!croot.isObject()) { + throw std::runtime_error(kInvalidFormatErr); + } + + if (auto& name = croot["Name"]; name.isString()) { + mName = name.asString(); + } else { + throw std::runtime_error(kInvalidFormatErr); + } + } + + CreateProjectSubfolders(*this); + + auto itemsDir = mRootPath / "items"; + ReadItemList(Products, itemsDir / "products.json"); + ReadItemList(Factories, itemsDir / "factories.json"); + ReadItemList(Customers, itemsDir / "customers.json"); + + Workflows.Reload(); + Templates.Reload(); +} + +Project::Project(fs::path rootPath, std::string name) + : mRootPath{ std::move(rootPath) } + , mRootPathString{ mRootPath.string() } + , mName{ std::move(name) } + , Workflows(*this) + , Templates(*this) + , Database(*this) +{ + CreateProjectSubfolders(*this); +} + +const fs::path& Project::GetPath() const +{ + return mRootPath; +} + +const std::string& Project::GetPathString() const +{ + return mRootPathString; +} + +fs::path Project::GetDatabasesDirectory() const +{ + return mRootPath / "databases"; +} + +fs::path Project::GetItemsDirectory() const +{ + return mRootPath / "items"; +} + +fs::path Project::GetWorkflowsDirectory() const +{ + return mRootPath / "workflows"; +} + +fs::path Project::GetWorkflowPath(std::string_view name) const +{ + return (mRootPath / "workflows" / name).concat(".cplt-workflow"); +} + +fs::path Project::GetTemplatesDirectory() const +{ + return mRootPath / "templates"; +} + +fs::path Project::GetTemplatePath(std::string_view name) const +{ + return (mRootPath / "templates" / name).concat(".cplt-template"); +} + +const std::string& Project::GetName() const +{ + return mName; +} + +void Project::SetName(std::string name) +{ + mName = std::move(name); +} + +Json::Value Project::Serialize() +{ + Json::Value root(Json::objectValue); + + root["Name"] = mName; + + return root; +} + +template <class T> +static void WriteItemList(ItemList<T>& list, const fs::path& filePath) +{ + std::ofstream ofs(filePath); + ofs << list.Serialize(); +} + +void Project::WriteToDisk() +{ + std::ofstream ofs(mRootPath / "cplt_project.json"); + ofs << this->Serialize(); + + auto itemsDir = GetItemsDirectory(); + WriteItemList(Products, itemsDir / "products.json"); + WriteItemList(Factories, itemsDir / "factories.json"); + WriteItemList(Customers, itemsDir / "customers.json"); +} diff --git a/app/source/Cplt/Model/Project.hpp b/app/source/Cplt/Model/Project.hpp new file mode 100644 index 0000000..8119a97 --- /dev/null +++ b/app/source/Cplt/Model/Project.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/Model/Database.hpp> +#include <Cplt/Model/Items.hpp> +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <json/forwards.h> +#include <tsl/array_map.h> +#include <filesystem> +#include <string> +#include <string_view> + +class Project +{ +private: + std::filesystem::path mRootPath; + std::string mRootPathString; + std::string mName; + + // (Exception to style guidelines) + // This is put after the private fields, so that when XxxDatabase's constructor runs, all of them will be initialized +public: + WorkflowAssetList Workflows; + TemplateAssetList Templates; + ItemList<ProductItem> Products; + ItemList<FactoryItem> Factories; + ItemList<CustomerItem> Customers; + MainDatabase Database; + +public: + /// Load the project from a directory containing the cplt_project.json file. + /// This only loads the main project file, the caller needs to + Project(std::filesystem::path rootPath); + + /// Create a project with the given name in the given path. Note that the path should be a directory that will contain the project files once created. + /// This function assumes the given directory will exist and is empty. + Project(std::filesystem::path rootPath, std::string name); + + /// Path to a *directory* that contains the project file. + const std::filesystem::path& GetPath() const; + const std::string& GetPathString() const; + + std::filesystem::path GetDatabasesDirectory() const; + std::filesystem::path GetItemsDirectory() const; + std::filesystem::path GetWorkflowsDirectory() const; + std::filesystem::path GetWorkflowPath(std::string_view name) const; + std::filesystem::path GetTemplatesDirectory() const; + std::filesystem::path GetTemplatePath(std::string_view name) const; + + const std::string& GetName() const; + void SetName(std::string name); + + Json::Value Serialize(); + void WriteToDisk(); +}; diff --git a/app/source/Cplt/Model/Template/TableTemplate.cpp b/app/source/Cplt/Model/Template/TableTemplate.cpp new file mode 100644 index 0000000..5cd9ed8 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplate.cpp @@ -0,0 +1,591 @@ +#include "TableTemplate.hpp" + +#include <Cplt/Utils/IO/StringIntegration.hpp> +#include <Cplt/Utils/IO/TslArrayIntegration.hpp> +#include <Cplt/Utils/IO/VectorIntegration.hpp> + +#include <xlsxwriter.h> +#include <algorithm> +#include <charconv> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <map> + +bool TableCell::IsDataHoldingCell() const +{ + return IsPrimaryCell() || !IsMergedCell(); +} + +bool TableCell::IsPrimaryCell() const +{ + return PrimaryCellLocation == Location; +} + +bool TableCell::IsMergedCell() const +{ + return PrimaryCellLocation.x == -1 || PrimaryCellLocation.y == -1; +} + +template <class TTableCell, class TStream> +void OperateStreamForTableCell(TTableCell& cell, TStream& proxy) +{ + proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Content); + proxy.Object(cell.Location); + proxy.Object(cell.PrimaryCellLocation); + proxy.Value(cell.SpanX); + proxy.Value(cell.SpanY); + proxy.Enum(cell.HorizontalAlignment); + proxy.Enum(cell.VerticalAlignment); + proxy.Enum(cell.Type); + proxy.Value(cell.DataId); +} + +void TableCell::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForTableCell(*this, stream); +} + +void TableCell::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForTableCell(*this, stream); +} + +Vec2i TableArrayGroup::GetLeftCell() const +{ + return { Row, LeftCell }; +} + +Vec2i TableArrayGroup::GetRightCell() const +{ + return { Row, RightCell }; +} + +int TableArrayGroup::GetCount() const +{ + return RightCell - LeftCell + 1; +} + +Vec2i TableArrayGroup::FindCell(std::string_view name) +{ + // TODO + return Vec2i{}; +} + +template <class TMap> +static bool UpdateElementName(TMap& map, std::string_view oldName, std::string_view newName) +{ + auto iter = map.find(oldName); + if (iter == map.end()) { + return false; + } + + auto elm = iter.value(); + auto [DISCARD, inserted] = map.insert(newName, elm); + if (!inserted) { + return false; + } + + map.erase(iter); + return true; +} + +bool TableArrayGroup::UpdateCellName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2Cell, oldName, newName); +} + +template <class TTableArrayGroup, class TStream> +void OperateStreamForTableArrayGroup(TTableArrayGroup& group, TStream& stream) +{ + stream.Value(group.Row); + stream.Value(group.LeftCell); + stream.Value(group.RightCell); +} + +void TableArrayGroup::ReadFromDataStream(InputDataStream& stream) +{ + ::OperateStreamForTableArrayGroup(*this, stream); +} + +void TableArrayGroup::WriteToDataStream(OutputDataStream& stream) const +{ + ::OperateStreamForTableArrayGroup(*this, stream); +} + +TableInstantiationParameters::TableInstantiationParameters(const TableTemplate& table) + : mTable{ &table } +{ +} + +TableInstantiationParameters& TableInstantiationParameters::ResetTable(const TableTemplate& newTable) +{ + mTable = &newTable; + return *this; +} + +TableInstantiationParameters TableInstantiationParameters::RebindTable(const TableTemplate& newTable) const +{ + TableInstantiationParameters result(newTable); + result.SingularCells = this->SingularCells; + result.ArrayGroups = this->ArrayGroups; + return result; +} + +const TableTemplate& TableInstantiationParameters::GetTable() const +{ + return *mTable; +} + +bool TableTemplate::IsInstance(const Template* tmpl) +{ + return tmpl->GetKind() == KD_Table; +} + +TableTemplate::TableTemplate() + : Template(KD_Table) +{ +} + +int TableTemplate::GetTableWidth() const +{ + return mColumnWidths.size(); +} + +int TableTemplate::GetTableHeight() const +{ + return mRowHeights.size(); +} + +void TableTemplate::Resize(int newWidth, int newHeight) +{ + // TODO this doesn't gracefully handle resizing to a smaller size which trims some merged cells + + std::vector<TableCell> cells; + cells.reserve(newWidth * newHeight); + + int tableWidth = GetTableWidth(); + int tableHeight = GetTableHeight(); + + for (int y = 0; y < newHeight; ++y) { + if (y >= tableHeight) { + for (int x = 0; x < newWidth; ++x) { + cells.push_back(TableCell{}); + } + continue; + } + + for (int x = 0; x < newWidth; ++x) { + if (x >= tableWidth) { + cells.push_back(TableCell{}); + } else { + auto& cell = GetCell({ x, y }); + cells.push_back(std::move(cell)); + } + } + } + + mCells = std::move(cells); + mColumnWidths.resize(newWidth, 80); + mRowHeights.resize(newHeight, 20); +} + +int TableTemplate::GetRowHeight(int row) const +{ + return mRowHeights[row]; +} + +void TableTemplate::SetRowHeight(int row, int height) +{ + mRowHeights[row] = height; +} + +int TableTemplate::GetColumnWidth(int column) const +{ + return mColumnWidths[column]; +} + +void TableTemplate::SetColumnWidth(int column, int width) +{ + mColumnWidths[column] = width; +} + +const TableCell& TableTemplate::GetCell(Vec2i pos) const +{ + int tableWidth = GetTableWidth(); + return mCells[pos.y * tableWidth + pos.x]; +} + +TableCell& TableTemplate::GetCell(Vec2i pos) +{ + return const_cast<TableCell&>(const_cast<const TableTemplate*>(this)->GetCell(pos)); +} + +void TableTemplate::SetCellType(Vec2i pos, TableCell::CellType type) +{ + auto& cell = GetCell(pos); + if (cell.Type == type) { + return; + } + + switch (cell.Type) { + // Nothing to change + case TableCell::ConstantCell: break; + + case TableCell::SingularParametricCell: + mName2Parameters.erase(cell.Content); + break; + + case TableCell::ArrayParametricCell: { + auto& ag = mArrayGroups[cell.DataId]; + if (pos.x == ag.LeftCell) { + ag.LeftCell++; + } else if (pos.x == ag.RightCell) { + ag.RightCell--; + } else { + } + } break; + } + + switch (type) { + // Nothing to do + case TableCell::ConstantCell: break; + + case TableCell::SingularParametricCell: { + int idx = pos.y * GetTableWidth() + pos.x; + auto [DISCARD, inserted] = mName2Parameters.insert(cell.Content, idx); + + // Duplicate name + if (!inserted) { + return; + } + } break; + + case TableCell::ArrayParametricCell: { + auto ptr = AddArrayGroup(pos.y, pos.x, pos.x); + + // Duplicate name + if (ptr == nullptr) { + return; + } + } break; + } + + cell.Type = type; +} + +bool TableTemplate::UpdateParameterName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2Parameters, oldName, newName); +} + +int TableTemplate::GetArrayGroupCount() const +{ + return mArrayGroups.size(); +} + +const TableArrayGroup& TableTemplate::GetArrayGroup(int id) const +{ + return mArrayGroups[id]; +} + +TableArrayGroup& TableTemplate::GetArrayGroup(int id) +{ + return mArrayGroups[id]; +} + +TableArrayGroup* TableTemplate::AddArrayGroup(int row, int left, int right) +{ + // size_t max value: 18446744073709551615 + // ^~~~~~~~~~~~~~~~~~~~ 20 chars + char name[20]; + auto res = std::to_chars(std::begin(name), std::end(name), mArrayGroups.size()); + std::string_view nameStr(name, res.ptr - name); + + return AddArrayGroup(nameStr, row, left, right); +} + +TableArrayGroup* TableTemplate::AddArrayGroup(std::string_view name, int row, int left, int right) +{ + assert(row >= 0 && row < GetTableHeight()); + assert(left >= 0 && left < GetTableWidth()); + assert(right >= 0 && right < GetTableWidth()); + + // TODO check for overlap + + if (left > right) { + std::swap(left, right); + } + + auto [DISCARD, inserted] = mName2ArrayGroups.insert(name, (int)mArrayGroups.size()); + if (!inserted) { + return nullptr; + } + + mArrayGroups.push_back(TableArrayGroup{ + .Row = row, + .LeftCell = left, + .RightCell = right, + }); + auto& ag = mArrayGroups.back(); + + for (int x = left; x <= right; x++) { + auto& cell = GetCell({ x, row }); + + // Update type + cell.Type = TableCell::ArrayParametricCell; + + // Insert parameter name lookup + while (true) { + auto [DISCARD, inserted] = ag.mName2Cell.insert(cell.Content, x); + if (inserted) { + break; + } + + cell.Content += "-"; + } + } + + return &ag; +} + +bool TableTemplate::UpdateArrayGroupName(std::string_view oldName, std::string_view newName) +{ + return ::UpdateElementName(mName2ArrayGroups, oldName, newName); +} + +bool TableTemplate::ExtendArrayGroupLeft(int id, int n) +{ + assert(n > 0); + + auto& ag = mArrayGroups[id]; + ag.LeftCell -= n; + + return false; +} + +bool TableTemplate::ExtendArrayGroupRight(int id, int n) +{ + assert(n > 0); + + auto& ag = mArrayGroups[id]; + ag.RightCell += n; + + return false; +} + +TableCell* TableTemplate::FindCell(std::string_view name) +{ + auto iter = mName2Parameters.find(name); + if (iter != mName2Parameters.end()) { + return &mCells[iter.value()]; + } else { + return nullptr; + } +} + +TableArrayGroup* TableTemplate::FindArrayGroup(std::string_view name) +{ + auto iter = mName2ArrayGroups.find(name); + if (iter != mName2ArrayGroups.end()) { + return &mArrayGroups[iter.value()]; + } else { + return nullptr; + } +} + +TableTemplate::MergeCellsResult TableTemplate::MergeCells(Vec2i topLeft, Vec2i bottomRight) +{ + auto SortTwo = [](int& a, int& b) { + if (a > b) { + std::swap(a, b); + } + }; + SortTwo(topLeft.x, bottomRight.x); + SortTwo(topLeft.y, bottomRight.y); + + auto ResetProgress = [&]() { + for (int y = topLeft.y; y < bottomRight.y; ++y) { + for (int x = topLeft.x; x < bottomRight.x; ++x) { + auto& cell = GetCell({ x, y }); + cell.PrimaryCellLocation = { -1, -1 }; + } + } + }; + + for (int y = topLeft.y; y < bottomRight.y; ++y) { + for (int x = topLeft.x; x < bottomRight.x; ++x) { + auto& cell = GetCell({ x, y }); + if (cell.IsMergedCell()) { + ResetProgress(); + return MCR_CellAlreadyMerged; + } + + cell.PrimaryCellLocation = topLeft; + } + } + + auto& primaryCell = GetCell(topLeft); + primaryCell.SpanX = bottomRight.x - topLeft.x; + primaryCell.SpanY = bottomRight.y - topLeft.y; + + return MCR_Success; +} + +TableTemplate::BreakCellsResult TableTemplate::BreakCells(Vec2i topLeft) +{ + auto& primaryCell = GetCell(topLeft); + if (!primaryCell.IsMergedCell()) { + return BCR_CellNotMerged; + } + + for (int dy = 0; dy < primaryCell.SpanY; ++dy) { + for (int dx = 0; dx < primaryCell.SpanX; ++dx) { + auto& cell = GetCell({ topLeft.x + dx, topLeft.y + dy }); + cell.PrimaryCellLocation = { -1, -1 }; + } + } + + primaryCell.SpanX = 1; + primaryCell.SpanY = 1; + + return BCR_Success; +} + +lxw_workbook* TableTemplate::InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const +{ + auto workbook = workbook_new("Table.xlsx"); + InstantiateToExcelWorksheet(workbook, params); + return workbook; +} + +lxw_worksheet* TableTemplate::InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const +{ + auto worksheet = workbook_add_worksheet(workbook, "CpltExport.xlsx"); + + // Map: row number -> length of generated ranges + std::map<int, int> generatedRanges; + + for (size_t i = 0; i < mArrayGroups.size(); ++i) { + auto& info = mArrayGroups[i]; + auto& param = params.ArrayGroups[i]; + + auto iter = generatedRanges.find(i); + if (iter != generatedRanges.end()) { + int available = iter->second; + if (available >= param.size()) { + // Current space is enough to fit in this array group, skip + continue; + } + } + + // Not enough space to fit in this array group, update (or insert) the appropriate amount of generated rows + int row = i; + int count = param.size(); + generatedRanges.try_emplace(row, count); + } + + auto GetOffset = [&](int y) -> int { + // std::find_if <values less than y> + int verticalOffset = 0; + for (auto it = generatedRanges.begin(); it != generatedRanges.end() && it->first < y; ++it) { + verticalOffset += it->second; + } + return verticalOffset; + }; + + auto WriteCell = [&](int row, int col, const TableCell& cell, const char* text) -> void { + if (cell.IsPrimaryCell()) { + int lastRow = row + cell.SpanY - 1; + int lastCol = col + cell.SpanX - 1; + // When both `string` and `format` are null, the top-left cell contents are untouched (what we just wrote in the above switch) + worksheet_merge_range(worksheet, row, col, lastRow, lastCol, text, nullptr); + } else { + worksheet_write_string(worksheet, row, col, text, nullptr); + } + }; + + // Write/instantiate all array groups + for (size_t i = 0; i < mArrayGroups.size(); ++i) { + auto& groupInfo = mArrayGroups[i]; + auto& groupParams = params.ArrayGroups[i]; + + int rowCellCount = groupInfo.GetCount(); + int rowCount = groupParams.size(); + int baseRowIdx = groupInfo.Row + GetOffset(groupInfo.Row); + + // For each row that would be generated + for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) { + auto& row = groupParams[rowIdx]; + + // For each cell in the row + for (int rowCellIdx = 0; rowCellIdx < rowCellCount; ++rowCellIdx) { + // TODO support merged cells in array groups + worksheet_write_string(worksheet, baseRowIdx + rowIdx, rowCellIdx, row[rowCellIdx].c_str(), nullptr); + } + } + } + + int tableWidth = GetTableWidth(); + int tableHeight = GetTableHeight(); + + // Write all regular and singular parameter cells + for (int y = 0; y < tableHeight; ++y) { + for (int x = 0; x < tableWidth; ++x) { + auto& cell = GetCell({ x, y }); + + if (!cell.IsDataHoldingCell()) { + continue; + } + + switch (cell.Type) { + case TableCell::ConstantCell: { + int row = y + GetOffset(y); + int col = x; + + WriteCell(row, col, cell, cell.Content.c_str()); + } break; + + case TableCell::SingularParametricCell: { + int row = y + GetOffset(y); + int col = x; + + auto iter = params.SingularCells.find({ x, y }); + if (iter != params.SingularCells.end()) { + WriteCell(row, col, cell, iter.value().c_str()); + } + } break; + + // See loop above that processes whole array groups at the same time + case TableCell::ArrayParametricCell: break; + } + } + } + + return worksheet; +} + +class TableTemplate::Private +{ +public: + template <class TTableTemplate, class TProxy> + static void OperateStream(TTableTemplate& table, TProxy& proxy) + { + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mColumnWidths); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mRowHeights); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mCells); + proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mArrayGroups); + proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2Parameters); + proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2ArrayGroups); + } +}; + +void TableTemplate::ReadFromDataStream(InputDataStream& stream) +{ + Private::OperateStream(*this, stream); +} + +void TableTemplate::WriteToDataStream(OutputDataStream& stream) const +{ + Private::OperateStream(*this, stream); +} diff --git a/app/source/Cplt/Model/Template/TableTemplate.hpp b/app/source/Cplt/Model/Template/TableTemplate.hpp new file mode 100644 index 0000000..3e931d4 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplate.hpp @@ -0,0 +1,223 @@ +#pragma once + +#include <Cplt/Model/Template/Template.hpp> +#include <Cplt/Utils/Vector.hpp> +#include <Cplt/Utils/VectorHash.hpp> +#include <Cplt/fwd.hpp> + +#include <tsl/array_map.h> +#include <tsl/robin_map.h> +#include <string> +#include <string_view> +#include <vector> + +class TableCell +{ +public: + enum TextAlignment + { + /// For horizontal alignment, this means align left. For vertical alignment, this means align top. + AlignAxisMin, + /// Align middle of the text to the middle of the axis. + AlignCenter, + /// For horizontal alignment, this means align right. For vertical alignment, this means align bottom. + AlignAxisMax, + }; + + enum CellType + { + ConstantCell, + SingularParametricCell, + ArrayParametricCell, + }; + +public: + /// Display content of this cell. This doesn't necessarily have to line up with the parameter name (if this cell is one). + std::string Content; + Vec2i Location; + /// Location of the primary (top left) cell, if this cell is a part of a merged group. + /// Otherwise, either component of this field shall be -1. + Vec2i PrimaryCellLocation{ -1, -1 }; + int SpanX = 0; + int SpanY = 0; + TextAlignment HorizontalAlignment = AlignCenter; + TextAlignment VerticalAlignment = AlignCenter; + CellType Type = ConstantCell; + /// The id of the group description object, if this cell isn't a constant or singular parameter cell. Otherwise, this value is -1. + int DataId = -1; + +public: + /// Return whether this cell holds meaningful data, i.e. true when this cell is either unmerged or the primary cell of a merged range. + bool IsDataHoldingCell() const; + /// Return whether this cell is the primary (i.e. top left) cell of a merged range or not. + bool IsPrimaryCell() const; + /// Return whether this cell is a part of a merged range or not. Includes the primary cell. + bool IsMergedCell() const; + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +// TODO support reverse (bottom to top) filling order +// TODO support horizontal filling order + +/// Parameter group information for a grouped array of cells. When instantiated, an array of 0 or more +/// elements shall be provided by the user, which will replace the group of templated cells with a list +/// of rows, each instantiated with the n-th element in the provided array. +/// \code +/// [["foo", "bar", "foobar"], +/// ["a", "b", c"], +/// ["1", "2", "3"], +/// ["x", "y", "z"]] +/// // ... may be more +/// \endcode +/// This would create 4 rows of data in the place of the original parameter group. +/// +/// If more than one array parameter groups are on the same row, they would share space between each other: +/// \code +/// | 2 elements was fed to it +/// | | 1 element was fed to it +/// V V +/// {~~~~~~~~~~~~~~~~}{~~~~~~~~~~~~~~} +/// +------+---------+---------------+ +/// | Foo | Example | Another group | +/// +------+---------+---------------+ +/// | Cool | Example | | +/// +------+---------+---------------+ +/// \endcode +/// +/// \see TableCell +/// \see TableInstantiationParameters +/// \see TableTemplate +class TableArrayGroup +{ +public: + /// Parameter name mapped to cell location (index from LeftCell). + tsl::array_map<char, int> mName2Cell; + int Row; + /// Leftmost cell in this group + int LeftCell; + /// Rightmost cell in this group + int RightCell; + +public: + Vec2i GetLeftCell() const; + Vec2i GetRightCell() const; + int GetCount() const; + + /// Find the location of the cell within this array group that has the given name. + Vec2i FindCell(std::string_view name); + bool UpdateCellName(std::string_view oldName, std::string_view newName); + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; +}; + +// Forward declaration of libxlsxwriter structs +struct lxw_workbook; +struct lxw_worksheet; + +/// An object containing the necessary information to instantiate a table template. +/// \see TableTemplate +class TableInstantiationParameters +{ +private: + const TableTemplate* mTable; + +public: + tsl::robin_map<Vec2i, std::string> SingularCells; + + using ArrayGroupRow = std::vector<std::string>; + using ArrayGroupData = std::vector<ArrayGroupRow>; + std::vector<ArrayGroupData> ArrayGroups; + +public: + TableInstantiationParameters(const TableTemplate& table); + + TableInstantiationParameters& ResetTable(const TableTemplate& newTable); + TableInstantiationParameters RebindTable(const TableTemplate& newTable) const; + + const TableTemplate& GetTable() const; +}; + +/// A table template, where individual cells can be filled by workflows instantiating this template. Merged cells, +/// parametric rows/columns, and grids are also supported. +/// +/// This current supports exporting to xlsx files. +class TableTemplate : public Template +{ + friend class TableSingleParamsIter; + friend class TableArrayGroupsIter; + class Private; + +private: + /// Map from parameter name to index of the parameter cell (stored in mCells). + tsl::array_map<char, int> mName2Parameters; + /// Map from array group name to the index of the array group (stored in mArrayGroups). + tsl::array_map<char, int> mName2ArrayGroups; + std::vector<TableCell> mCells; + std::vector<TableArrayGroup> mArrayGroups; + std::vector<int> mRowHeights; + std::vector<int> mColumnWidths; + +public: + static bool IsInstance(const Template* tmpl); + TableTemplate(); + + int GetTableWidth() const; + int GetTableHeight() const; + void Resize(int newWidth, int newHeight); + + int GetRowHeight(int row) const; + void SetRowHeight(int row, int height); + int GetColumnWidth(int column) const; + void SetColumnWidth(int column, int width); + + const TableCell& GetCell(Vec2i pos) const; + TableCell& GetCell(Vec2i pos); + /// <ul> + /// <li> In case of becoming a SingularParametricCell: the parameter name is filled with TableCell::Content. + /// <li> In case of becoming a ArrayGroupParametricCell: the array group name is automatically generated as the nth group it would be come. + /// i.e., if there aRe currently 3 groups, the newly created group would be named "4". + /// If this name collides with an existing group, hyphens \c - will be append to the name until no collision happens. + /// </ul> + void SetCellType(Vec2i pos, TableCell::CellType type); + + /// Updates the parameter cell to a new name. Returns true on success and false on failure (param not found or name duplicates). + bool UpdateParameterName(std::string_view oldName, std::string_view newName); + + int GetArrayGroupCount() const; + const TableArrayGroup& GetArrayGroup(int id) const; + TableArrayGroup& GetArrayGroup(int id); + TableArrayGroup* AddArrayGroup(int row, int left, int right); + TableArrayGroup* AddArrayGroup(std::string_view name, int row, int left, int right); + bool UpdateArrayGroupName(std::string_view oldName, std::string_view newName); + bool ExtendArrayGroupLeft(int id, int n); + bool ExtendArrayGroupRight(int id, int n); + + /// Find a singular parameter cell by its name. This does not include cells within an array group. + TableCell* FindCell(std::string_view name); + + /// Find an array group by its name. + TableArrayGroup* FindArrayGroup(std::string_view name); + + enum MergeCellsResult + { + MCR_CellAlreadyMerged, + MCR_Success, + }; + MergeCellsResult MergeCells(Vec2i topLeft, Vec2i bottomRight); + + enum BreakCellsResult + { + BCR_CellNotMerged, + BCR_Success, + }; + BreakCellsResult BreakCells(Vec2i topLeft); + + lxw_workbook* InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const; + lxw_worksheet* InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const; + + void ReadFromDataStream(InputDataStream& stream) override; + void WriteToDataStream(OutputDataStream& stream) const override; +}; diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.cpp b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp new file mode 100644 index 0000000..19e30b9 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp @@ -0,0 +1,52 @@ +#include "TableTemplateIterator.hpp" + +TableSingleParamsIter::TableSingleParamsIter(TableTemplate& tmpl) + : mTemplate{ &tmpl } + , mIter{ tmpl.mName2Parameters.begin() } +{ +} + +bool TableSingleParamsIter::HasNext() const +{ + return mIter != mTemplate->mName2Parameters.end(); +} + +TableCell& TableSingleParamsIter::Next() +{ + int id = mIter.value(); + ++mIter; + + return mTemplate->mCells[id]; +} + +TableArrayGroupsIter::TableArrayGroupsIter(TableTemplate& tmpl) + : mTemplate{ &tmpl } + , mIter{ tmpl.mName2ArrayGroups.begin() } +{ +} + +bool TableArrayGroupsIter::HasNext() const +{ + return mIter != mTemplate->mName2ArrayGroups.end(); +} + +TableArrayGroup& TableArrayGroupsIter::Peek() const +{ + int id = mIter.value(); + return mTemplate->mArrayGroups[id]; +} + +std::string_view TableArrayGroupsIter::PeekName() const +{ + return mIter.key_sv(); +} + +const char* TableArrayGroupsIter::PeekNameCStr() const +{ + return mIter.key(); +} + +void TableArrayGroupsIter::Next() +{ + ++mIter; +} diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.hpp b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp new file mode 100644 index 0000000..c4b5bf9 --- /dev/null +++ b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Model/Template/Template.hpp> + +#include <string_view> + +class TableSingleParamsIter +{ +private: + TableTemplate* mTemplate; + tsl::array_map<char, int>::iterator mIter; + +public: + TableSingleParamsIter(TableTemplate& tmpl); + + bool HasNext() const; + TableCell& Next(); +}; + +class TableArrayGroupsIter +{ +private: + TableTemplate* mTemplate; + tsl::array_map<char, int>::iterator mIter; + +public: + TableArrayGroupsIter(TableTemplate& tmpl); + + bool HasNext() const; + TableArrayGroup& Peek() const; + std::string_view PeekName() const; + const char* PeekNameCStr() const; + void Next(); +}; diff --git a/app/source/Cplt/Model/Template/Template.hpp b/app/source/Cplt/Model/Template/Template.hpp new file mode 100644 index 0000000..cf926d0 --- /dev/null +++ b/app/source/Cplt/Model/Template/Template.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/fwd.hpp> + +#include <filesystem> +#include <iosfwd> +#include <memory> +#include <string> + +class Template : public Asset +{ +public: + enum Kind + { + KD_Table, + + InvalidKind, + KindCount = InvalidKind, + }; + + using CategoryType = TemplateAssetList; + +private: + Kind mKind; + +public: + static const char* FormatKind(Kind kind); + static std::unique_ptr<Template> CreateByKind(Kind kind); + + static bool IsInstance(const Template* tmpl); + + Template(Kind kind); + ~Template() override = default; + + Kind GetKind() const; + + virtual void ReadFromDataStream(InputDataStream& stream) = 0; + virtual void WriteToDataStream(OutputDataStream& stream) const = 0; +}; + +class TemplateAssetList final : public AssetListTyped<Template> +{ +private: + // AC = Asset Creator + std::string mACNewName; + NameSelectionError mACNewNameError = NameSelectionError::Empty; + Template::Kind mACNewKind = Template::InvalidKind; + +public: + // Inherit constructors + using AssetListTyped::AssetListTyped; + +protected: + void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override; + + std::string RetrieveNameFromFile(const std::filesystem::path& file) const override; + uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override; + std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override; + + bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override; + Template* LoadInstance(const SavedAsset& assetInfo) const override; + Template* CreateInstance(const SavedAsset& assetInfo) const override; + bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override; + + void DisplayAssetCreator(ListState& state) override; + void DisplayDetailsTable(ListState& state) const override; +}; diff --git a/app/source/Cplt/Model/Template/Template_Main.cpp b/app/source/Cplt/Model/Template/Template_Main.cpp new file mode 100644 index 0000000..d658231 --- /dev/null +++ b/app/source/Cplt/Model/Template/Template_Main.cpp @@ -0,0 +1,214 @@ +#include "Template.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/Archive.hpp> +#include <Cplt/Utils/UUID.hpp> + +#include <imgui.h> +#include <imgui_stdlib.h> +#include <algorithm> +#include <cstdint> +#include <fstream> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; + +Template::Template(Kind kind) + : mKind{ kind } +{ +} + +Template::Kind Template::GetKind() const +{ + return mKind; +} + +void TemplateAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const +{ + auto dir = GetConnectedProject().GetTemplatesDirectory(); + DiscoverFilesByExtension(callback, dir, ".cplt-template"sv); +} + +std::string TemplateAssetList::RetrieveNameFromFile(const fs::path& file) const +{ + auto res = DataArchive::LoadFile(file); + if (!res) return ""; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + return assetInfo.Name; +} + +uuids::uuid TemplateAssetList::RetrieveUuidFromFile(const fs::path& file) const +{ + return uuids::uuid::from_string(file.stem().string()); +} + +fs::path TemplateAssetList::RetrievePathFromAsset(const SavedAsset& asset) const +{ + auto fileName = uuids::to_string(asset.Uuid); + return GetConnectedProject().GetTemplatePath(fileName); +} + +bool TemplateAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const +{ + auto path = RetrievePathFromAsset(assetInfo); + auto res = DataArchive::SaveFile(path); + if (!res) return false; + auto& stream = res.value(); + + stream.WriteObject(assetInfo); + // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Template + if (auto tmpl = static_cast<const Template*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + stream.WriteObject(*tmpl); + } + + return true; +} + +static std::unique_ptr<Template> LoadTemplateFromFile(const fs::path& path) +{ + auto res = DataArchive::LoadFile(path); + if (!res) return nullptr; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + auto kind = static_cast<Template::Kind>(assetInfo.Payload); + auto tmpl = Template::CreateByKind(kind); + stream.ReadObject(*tmpl); + + return tmpl; +} + +Template* TemplateAssetList::LoadInstance(const SavedAsset& assetInfo) const +{ + return ::LoadTemplateFromFile(RetrievePathFromAsset(assetInfo)).release(); +} + +Template* TemplateAssetList::CreateInstance(const SavedAsset& assetInfo) const +{ + auto kind = static_cast<Template::Kind>(assetInfo.Payload); + return Template::CreateByKind(kind).release(); +} + +bool TemplateAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const +{ + // Get asset path, which is only dependent on UUID + auto path = RetrievePathFromAsset(assetInfo); + + auto tmpl = ::LoadTemplateFromFile(path); + if (!tmpl) return false; + + // Rewrite the asset with the updated name (note the given assetInfo already has the update name) + SaveInstance(assetInfo, tmpl.get()); + + return true; +} + +void TemplateAssetList::DisplayAssetCreator(ListState& state) +{ + auto ValidateNewName = [&]() -> void { + if (mACNewName.empty()) { + mACNewNameError = NameSelectionError::Empty; + return; + } + + if (FindByName(mACNewName)) { + mACNewNameError = NameSelectionError::Duplicated; + return; + } + + mACNewNameError = NameSelectionError::None; + }; + + auto ShowNewNameErrors = [&]() -> void { + switch (mACNewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + }; + + auto ShowNewKindErrors = [&]() -> void { + if (mACNewKind == Template::InvalidKind) { + ImGui::ErrorMessage(I18N_TEXT("Invalid template type", L10N_TEMPLATE_INVALID_TYPE_ERROR)); + } + }; + + auto IsInputValid = [&]() -> bool { + return mACNewNameError == NameSelectionError::None && + mACNewKind != Template::InvalidKind; + }; + + auto ResetState = [&]() -> void { + mACNewName.clear(); + mACNewKind = Template::InvalidKind; + ValidateNewName(); + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) { + ValidateNewName(); + } + + if (ImGui::BeginCombo(I18N_TEXT("Type", L10N_TYPE), Template::FormatKind(mACNewKind))) { + for (int i = 0; i < Template::KindCount; ++i) { + auto kind = static_cast<Template::Kind>(i); + if (ImGui::Selectable(Template::FormatKind(kind), mACNewKind == kind)) { + mACNewKind = kind; + } + } + ImGui::EndCombo(); + } + + ShowNewNameErrors(); + ShowNewKindErrors(); + + if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) { + ImGui::CloseCurrentPopup(); + + Create(SavedAsset{ + .Name = mACNewName, + .Payload = static_cast<uint64_t>(mACNewKind), + }); + ResetState(); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } +} + +void TemplateAssetList::DisplayDetailsTable(ListState& state) const +{ + ImGui::BeginTable("AssetDetailsTable", 2, ImGuiTableFlags_Borders); + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME)); + ImGui::TableSetupColumn(I18N_TEXT("Type", L10N_TYPE)); + ImGui::TableHeadersRow(); + + for (auto& asset : this->GetAssets()) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) { + state.SelectedAsset = &asset; + } + + ImGui::TableNextColumn(); + auto kind = static_cast<Template::Kind>(asset.Payload); + ImGui::TextUnformatted(Template::FormatKind(kind)); + } + + ImGui::EndTable(); +} diff --git a/app/source/Cplt/Model/Template/Template_RTTI.cpp b/app/source/Cplt/Model/Template/Template_RTTI.cpp new file mode 100644 index 0000000..a96680b --- /dev/null +++ b/app/source/Cplt/Model/Template/Template_RTTI.cpp @@ -0,0 +1,29 @@ +#include "Template.hpp" + +#include <Cplt/Model/Template/TableTemplate.hpp> +#include <Cplt/Utils/I18n.hpp> + +const char* Template::FormatKind(Kind kind) +{ + switch (kind) { + case KD_Table: return I18N_TEXT("Table template", L10N_TEMPLATE_TABLE); + + case InvalidKind: break; + } + return ""; +} + +std::unique_ptr<Template> Template::CreateByKind(Kind kind) +{ + switch (kind) { + case KD_Table: return std::make_unique<TableTemplate>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool Template::IsInstance(const Template* tmpl) +{ + return true; +} diff --git a/app/source/Cplt/Model/Template/fwd.hpp b/app/source/Cplt/Model/Template/fwd.hpp new file mode 100644 index 0000000..8378871 --- /dev/null +++ b/app/source/Cplt/Model/Template/fwd.hpp @@ -0,0 +1,11 @@ +#pragma once + +// TableTemplate.hpp +class TableCell; +class TableArrayGroup; +class TableInstantiationParameters; +class TableTemplate; + +// Template.hpp +class Template; +class TemplateAssetList; diff --git a/app/source/Cplt/Model/Workflow/Evaluation.cpp b/app/source/Cplt/Model/Workflow/Evaluation.cpp new file mode 100644 index 0000000..7035bf9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Evaluation.cpp @@ -0,0 +1,174 @@ +#include "Evaluation.hpp" + +#include <queue> + +const char* WorkflowEvaluationError::FormatMessageType(enum MessageType messageType) +{ + switch (messageType) { + case Error: return "Error"; + case Warning: return "Warning"; + } +} + +const char* WorkflowEvaluationError::FormatPinType(enum PinType pinType) +{ + switch (pinType) { + case NoPin: return nullptr; + case InputPin: return "Input pin"; + case OutputPin: return "Output pin"; + } +} + +std::string WorkflowEvaluationError::Format() const +{ + // TODO convert to std::format + + std::string result; + result += FormatMessageType(this->Type); + result += " at "; + result += NodeId; + if (auto pinText = FormatPinType(this->PinType)) { + result += "/"; + result += pinText; + result += " "; + result += PinId; + } + result += ": "; + result += this->Message; + + return result; +} + +struct WorkflowEvaluationContext::RuntimeNode +{ + enum EvaluationStatus + { + ST_Unevaluated, + ST_Success, + ST_Failed, + }; + + EvaluationStatus Status = ST_Unevaluated; +}; + +struct WorkflowEvaluationContext::RuntimeConnection +{ + std::unique_ptr<BaseValue> Value; + + bool IsAvailableValue() const + { + return Value != nullptr; + } +}; + +WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow) + : mWorkflow{ &workflow } +{ + mRuntimeNodes.resize(workflow.mNodes.size()); + mRuntimeConnections.resize(workflow.mConnections.size()); +} + +BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant) +{ + if (constant) { + return mWorkflow->GetConstantById(id); + } else { + return mRuntimeConnections[id].Value.get(); + } +} + +BaseValue* WorkflowEvaluationContext::GetConnectionValue(const WorkflowNode::InputPin& inputPin) +{ + if (inputPin.IsConnected()) { + return GetConnectionValue(inputPin.Connection, inputPin.IsConstantConnection()); + } else { + return nullptr; + } +} + +void WorkflowEvaluationContext::SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value) +{ + mRuntimeConnections[id].Value = std::move(value); +} + +void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value) +{ + if (outputPin.IsConnected()) { + SetConnectionValue(outputPin.Connection, std::move(value)); + } +} + +void WorkflowEvaluationContext::Run() +{ + int evaluatedCount = 0; + int erroredCount = 0; + + for (auto& depthGroup : mWorkflow->GetDepthGroups()) { + for (size_t idx : depthGroup) { + auto& rn = mRuntimeNodes[idx]; + auto& n = *mWorkflow->mNodes[idx]; + + // TODO + + int preEvalErrors = mErrors.size(); + n.Evaluate(*this); + if (preEvalErrors != mErrors.size()) { + erroredCount++; + } else { + evaluatedCount++; + } + } + } + + for (size_t i = 0; i < mRuntimeNodes.size(); ++i) { + auto& rn = mRuntimeNodes[i]; + auto& n = *mWorkflow->mNodes[i]; + if (n.GetType() == WorkflowNode::OutputType) { + // TODO record outputs + } + } +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Error, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = pinId, + .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin, + .Type = WorkflowEvaluationError::Warning, + }); +} + +void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node) +{ + mErrors.push_back(WorkflowEvaluationError{ + .Message = std::move(message), + .NodeId = node.GetId(), + .PinId = -1, + .PinType = WorkflowEvaluationError::NoPin, + .Type = WorkflowEvaluationError::Warning, + }); +} diff --git a/app/source/Cplt/Model/Workflow/Evaluation.hpp b/app/source/Cplt/Model/Workflow/Evaluation.hpp new file mode 100644 index 0000000..5b8c6cc --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Evaluation.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <cstdint> +#include <string> +#include <vector> + +class WorkflowEvaluationError +{ +public: + enum MessageType : int16_t + { + Error, + Warning, + }; + + enum PinType : int16_t + { + NoPin, + InputPin, + OutputPin, + }; + +public: + std::string Message; + size_t NodeId; + int PinId; + PinType PinType; + MessageType Type; + +public: + static const char* FormatMessageType(enum MessageType messageType); + static const char* FormatPinType(enum PinType pinType); + + std::string Format() const; +}; + +class WorkflowEvaluationContext +{ +private: + struct RuntimeNode; + struct RuntimeConnection; + + Workflow* mWorkflow; + std::vector<RuntimeNode> mRuntimeNodes; + std::vector<RuntimeConnection> mRuntimeConnections; + std::vector<WorkflowEvaluationError> mErrors; + std::vector<WorkflowEvaluationError> mWarnings; + +public: + WorkflowEvaluationContext(Workflow& workflow); + + BaseValue* GetConnectionValue(size_t id, bool constant); + BaseValue* GetConnectionValue(const WorkflowNode::InputPin& inputPin); + void SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value); + void SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value); + + void ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportError(std::string message, const WorkflowNode& node); + void ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin); + void ReportWarning(std::string message, const WorkflowNode& node); + + /// Run until all possible paths have been evaluated. + void Run(); +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp new file mode 100644 index 0000000..df4a8bb --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp @@ -0,0 +1,18 @@ +#include "DocumentNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> + +bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_DocumentTemplateExpansion; +} + +DocumentTemplateExpansionNode::DocumentTemplateExpansionNode() + : WorkflowNode(KD_DocumentTemplateExpansion, false) +{ +} + +void DocumentTemplateExpansionNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp new file mode 100644 index 0000000..a266b2c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +class DocumentTemplateExpansionNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + DocumentTemplateExpansionNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp new file mode 100644 index 0000000..f8b29bb --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp @@ -0,0 +1,94 @@ +#include "NumericNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> +#include <Cplt/Utils/RTTI.hpp> + +#include <cassert> +#include <utility> + +WorkflowNode::Kind NumericOperationNode::OperationTypeToNodeKind(OperationType type) +{ + switch (type) { + case Addition: return KD_NumericAddition; + case Subtraction: return KD_NumericSubtraction; + case Multiplication: return KD_NumericMultiplication; + case Division: return KD_NumericDivision; + default: return InvalidKind; + } +} + +NumericOperationNode::OperationType NumericOperationNode::NodeKindToOperationType(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return Addition; + case KD_NumericSubtraction: return Subtraction; + case KD_NumericMultiplication: return Multiplication; + case KD_NumericDivision: return Division; + default: return InvalidType; + } +} + +bool NumericOperationNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() >= KD_NumericAddition && node->GetKind() <= KD_NumericDivision; +} + +NumericOperationNode::NumericOperationNode(OperationType type) + : WorkflowNode(OperationTypeToNodeKind(type), false) + , mType{ type } +{ + mInputs.resize(2); + mInputs[0].MatchingType = BaseValue::KD_Numeric; + mInputs[1].MatchingType = BaseValue::KD_Numeric; + + mOutputs.resize(1); + mOutputs[0].MatchingType = BaseValue::KD_Numeric; +} + +void NumericOperationNode::Evaluate(WorkflowEvaluationContext& ctx) +{ + auto lhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[0])); + if (!lhsVal) return; + double lhs = lhsVal->GetValue(); + + auto rhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[1])); + if (!rhsVal) return; + double rhs = rhsVal->GetValue(); + + double res; + switch (mType) { + case Addition: res = lhs + rhs; break; + case Subtraction: res = lhs - rhs; break; + case Multiplication: res = lhs * rhs; break; + case Division: { + if (rhs == 0.0) { + ctx.ReportError(I18N_TEXT("Error: division by 0", L10N_WORKFLOW_RTERROR_DIV_BY_0), *this); + return; + } + res = lhs / rhs; + } break; + + default: return; + } + + auto value = std::make_unique<NumericValue>(); + value->SetValue(res); + ctx.SetConnectionValue(mOutputs[0], std::move(value)); +} + +bool NumericExpressionNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_NumericExpression; +} + +NumericExpressionNode::NumericExpressionNode() + : WorkflowNode(KD_NumericExpression, false) +{ +} + +void NumericExpressionNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp new file mode 100644 index 0000000..3c89708 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class NumericOperationNode : public WorkflowNode +{ +public: + enum OperationType + { + Addition, + Subtraction, + Multiplication, + Division, + + InvalidType, + TypeCount = InvalidType, + }; + +private: + OperationType mType; + +public: + static Kind OperationTypeToNodeKind(OperationType type); + static OperationType NodeKindToOperationType(Kind kind); + static bool IsInstance(const WorkflowNode* node); + NumericOperationNode(OperationType type); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class NumericExpressionNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + NumericExpressionNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +};
\ No newline at end of file diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp new file mode 100644 index 0000000..9b31f7a --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp @@ -0,0 +1,231 @@ +#include "TextNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Utils/Macros.hpp> +#include <Cplt/Utils/RTTI.hpp> +#include <Cplt/Utils/Variant.hpp> + +#include <cassert> +#include <utility> +#include <variant> +#include <vector> + +class TextFormatterNode::Impl +{ +public: + template <class TFunction> + static void ForArguments(std::vector<Element>::iterator begin, std::vector<Element>::iterator end, const TFunction& func) + { + for (auto it = begin; it != end; ++it) { + auto& elm = *it; + if (auto arg = std::get_if<Argument>(&elm)) { + func(*arg); + } + } + } + + /// Find the pin index that the \c elmIdx -th element should have, based on the elements coming before it. + static int FindPinForElement(const std::vector<Element>& vec, int elmIdx) + { + for (int i = elmIdx; i >= 0; --i) { + auto& elm = vec[i]; + if (auto arg = std::get_if<Argument>(&elm)) { + return arg->PinIdx + 1; + } + } + return 0; + } +}; + +BaseValue::Kind TextFormatterNode::ArgumentTypeToValueKind(TextFormatterNode::ArgumentType arg) +{ + switch (arg) { + case NumericArgument: return BaseValue::KD_Numeric; + case TextArgument: return BaseValue::KD_Text; + case DateTimeArgument: return BaseValue::KD_DateTime; + } +} + +bool TextFormatterNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_TextFormatting; +} + +TextFormatterNode::TextFormatterNode() + : WorkflowNode(KD_TextFormatting, false) +{ +} + +int TextFormatterNode::GetElementCount() const +{ + return mElements.size(); +} + +const TextFormatterNode::Element& TextFormatterNode::GetElement(int idx) const +{ + return mElements[idx]; +} + +void TextFormatterNode::SetElement(int idx, std::string text) +{ + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { mMinOutputChars -= original.size(); }, + [&](const Argument& original) { PreRemoveElement(idx); }, + }, + mElements[idx]); + + mMinOutputChars += text.size(); + mElements[idx] = std::move(text); +} + +void TextFormatterNode::SetElement(int idx, ArgumentType argument) +{ + assert(idx >= 0 && idx < mElements.size()); + + std::visit( + Overloaded{ + [&](const std::string& original) { + mMinOutputChars -= original.size(); + + mElements[idx] = Argument{ + .Type = argument, + .PinIdx = Impl::FindPinForElement(mElements, idx), + }; + /* `original` is invalid from this point */ + }, + [&](const Argument& original) { + int pinIdx = original.PinIdx; + + // Create pin + auto& pin = mInputs[pinIdx]; + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements[idx] = Argument{ + .Type = argument, + .PinIdx = pinIdx, + }; + /* `original` is invalid from this point */ + }, + }, + mElements[idx]); +} + +void TextFormatterNode::InsertElement(int idx, std::string text) +{ + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(std::move(text)); + + mMinOutputChars += text.size(); + mElements.insert(mElements.begin() + idx, std::move(text)); +} + +void TextFormatterNode::InsertElement(int idx, ArgumentType argument) +{ + assert(idx >= 0); + if (idx >= mElements.size()) AppendElement(argument); + + int pinIdx = Impl::FindPinForElement(mElements, idx); + + // Create pin + auto& pin = InsertInputPin(pinIdx); + pin.MatchingType = ArgumentTypeToValueKind(argument); + + // Create element + mElements.insert( + mElements.begin() + idx, + Argument{ + .Type = argument, + .PinIdx = pinIdx, + }); +} + +void TextFormatterNode::AppendElement(std::string text) +{ + mMinOutputChars += text.size(); + mElements.push_back(std::move(text)); +} + +void TextFormatterNode::AppendElement(ArgumentType argument) +{ + int pinIdx = mInputs.size(); + // Create pin + mInputs.push_back(InputPin{}); + mInputs.back().MatchingType = ArgumentTypeToValueKind(argument); + // Creat eelement + mElements.push_back(Argument{ + .Type = argument, + .PinIdx = pinIdx, + }); +} + +void TextFormatterNode::RemoveElement(int idx) +{ + assert(idx >= 0 && idx < mElements.size()); + + PreRemoveElement(idx); + if (auto arg = std::get_if<Argument>(&mElements[idx])) { + RemoveInputPin(arg->PinIdx); + } + mElements.erase(mElements.begin() + idx); +} + +void TextFormatterNode::Evaluate(WorkflowEvaluationContext& ctx) +{ + std::string result; + result.reserve((size_t)(mMinOutputChars * 1.5f)); + + auto HandleText = [&](const std::string& str) { + result += str; + }; + auto HandleArgument = [&](const Argument& arg) { + switch (arg.Type) { + case NumericArgument: { + if (auto val = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-numeric value connected to a numeric text format parameter.", *this); + } + } break; + case TextArgument: { + if (auto val = dyn_cast<TextValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetValue(); + } else { + // TODO localize + ctx.ReportError("Non-text value connected to a textual text format parameter.", *this); + } + } break; + case DateTimeArgument: { + if (auto val = dyn_cast<DateTimeValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) { + result += val->GetString(); + } else { + // TODO localize + ctx.ReportError("Non-date/time value connected to a date/time text format parameter.", *this); + } + } break; + } + }; + + for (auto& elm : mElements) { + std::visit(Overloaded{ HandleText, HandleArgument }, elm); + } +} + +void TextFormatterNode::PreRemoveElement(int idx) +{ + auto& elm = mElements[idx]; + if (auto arg = std::get_if<Argument>(&elm)) { + RemoveInputPin(arg->PinIdx); + Impl::ForArguments( + mElements.begin() + idx + 1, + mElements.end(), + [&](Argument& arg) { + arg.PinIdx--; + }); + } +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp new file mode 100644 index 0000000..4689931 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +#include <cstddef> +#include <memory> +#include <variant> +#include <vector> + +class TextFormatterNode : public WorkflowNode +{ +public: + enum ArgumentType + { + NumericArgument, + TextArgument, + DateTimeArgument, + }; + +private: + class Impl; + + struct Argument + { + ArgumentType Type; + int PinIdx; + }; + using Element = std::variant<std::string, Argument>; + + std::vector<Element> mElements; + int mMinOutputChars; + +public: + static BaseValue::Kind ArgumentTypeToValueKind(ArgumentType arg); + static bool IsInstance(const WorkflowNode* node); + TextFormatterNode(); + + int GetElementCount() const; + const Element& GetElement(int idx) const; + + void SetElement(int idx, std::string text); + void SetElement(int idx, ArgumentType argument); + void InsertElement(int idx, std::string text); + void InsertElement(int idx, ArgumentType argument); + void AppendElement(std::string text); + void AppendElement(ArgumentType argument); + void RemoveElement(int idx); + + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; + +private: + void PreRemoveElement(int idx); +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp new file mode 100644 index 0000000..93d458c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp @@ -0,0 +1,32 @@ +#include "UserInputNodes.hpp" + +#include <Cplt/Model/Workflow/Evaluation.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> + +bool FormInputNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_FormInput; +} + +FormInputNode::FormInputNode() + : WorkflowNode(KD_FormInput, false) +{ +} + +void FormInputNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} + +bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node) +{ + return node->GetKind() == KD_DatabaseRowsInput; +} + +DatabaseRowsInputNode::DatabaseRowsInputNode() + : WorkflowNode(KD_DatabaseRowsInput, false) +{ +} + +void DatabaseRowsInputNode::Evaluate(WorkflowEvaluationContext& ctx) +{ +} diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp new file mode 100644 index 0000000..f0b923c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include <Cplt/Model/Workflow/Workflow.hpp> + +class FormInputNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + FormInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; + +class DatabaseRowsInputNode : public WorkflowNode +{ +public: + static bool IsInstance(const WorkflowNode* node); + DatabaseRowsInputNode(); + + // TODO + virtual void Evaluate(WorkflowEvaluationContext& ctx) override; +}; diff --git a/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp new file mode 100644 index 0000000..4153825 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp @@ -0,0 +1,15 @@ +#pragma once + +// DocumentNodes.hpp +class DocumentTemplateExpansionNode; + +// InputNodes.hpp +class FormInputNode; +class DatabaseRowsInputNode; + +// NumericNodes.hpp +class NumericOperationNode; +class NumericExpressionNode; + +// TextNodes.hpp +class TextFormatterNode; diff --git a/app/source/Cplt/Model/Workflow/Value.hpp b/app/source/Cplt/Model/Workflow/Value.hpp new file mode 100644 index 0000000..70fcb57 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include <Cplt/Utils/Color.hpp> +#include <Cplt/fwd.hpp> + +#include <iosfwd> +#include <memory> +#include <string> +#include <vector> + +class BaseValue +{ +public: + enum Kind + { + KD_Numeric, + KD_Text, + KD_DateTime, + KD_DatabaseRowId, + KD_List, + KD_Dictionary, + + KD_BaseObject, + KD_SaleDatabaseRow, + KD_PurchaseDatabaseRow, + KD_BaseObjectLast = KD_PurchaseDatabaseRow, + + /// An unspecified type, otherwise known as "any" in some contexts. + InvalidKind, + KindCount = InvalidKind, + }; + + struct KindInfo + { + ImGui::IconType PinIcon; + RgbaColor PinColor; + }; + +private: + Kind mKind; + +public: + static const KindInfo& QueryInfo(Kind kind); + static const char* Format(Kind kind); + static std::unique_ptr<BaseValue> CreateByKind(Kind kind); + + static bool IsInstance(const BaseValue* value); + + BaseValue(Kind kind); + virtual ~BaseValue() = default; + + BaseValue(const BaseValue&) = delete; + BaseValue& operator=(const BaseValue&) = delete; + BaseValue(BaseValue&&) = default; + BaseValue& operator=(BaseValue&&) = default; + + Kind GetKind() const; + + // TODO get constant editor + + /// The functions \c ReadFrom, \c WriteTo will only be valid to call if this function returns true. + virtual bool SupportsConstant() const; + virtual void ReadFrom(std::istream& stream); + virtual void WriteTo(std::ostream& stream); +}; + +class BaseObjectDescription +{ +public: + struct Property + { + std::string Name; + BaseValue::Kind Kind; + bool Mutatable = true; + }; + +public: + std::vector<Property> Properties; +}; + +class BaseObjectValue : public BaseValue +{ +public: + /// \param kind A value kind enum, within the range of KD_BaseObject and KD_BaseObjectLast (both inclusive). + static const BaseObjectDescription& QueryObjectInfo(Kind kind); + + static bool IsInstance(const BaseValue* value); + BaseObjectValue(Kind kind); + + const BaseObjectDescription& GetObjectDescription() const; + + virtual const BaseValue* GetProperty(int idx) const = 0; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value) = 0; +}; diff --git a/app/source/Cplt/Model/Workflow/ValueInternals.hpp b/app/source/Cplt/Model/Workflow/ValueInternals.hpp new file mode 100644 index 0000000..45842db --- /dev/null +++ b/app/source/Cplt/Model/Workflow/ValueInternals.hpp @@ -0,0 +1,21 @@ +// This file contains utility classes and macros for implementing values +// As consumers, you should not include this header as it contains unnecessary symbols and can pollute your files +// for this reason, classes here aren't forward-declared in fwd.hpp either. + +#pragma once + +#include <Cplt/Utils/RTTI.hpp> + +#include <utility> + +#define CHECK_VALUE_TYPE(Type, value) \ + if (!is_a<Type>(value)) { \ + return false; \ + } + +#define CHECK_VALUE_TYPE_AND_MOVE(Type, dest, value) \ + if (auto ptr = dyn_cast<Type>(value)) { \ + dest = std::move(*ptr); \ + } else { \ + return false; \ + } diff --git a/app/source/Cplt/Model/Workflow/Value_Main.cpp b/app/source/Cplt/Model/Workflow/Value_Main.cpp new file mode 100644 index 0000000..ca972c4 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value_Main.cpp @@ -0,0 +1,35 @@ +#include "Value.hpp" + +BaseValue::BaseValue(Kind kind) + : mKind{ kind } +{ +} + +BaseValue::Kind BaseValue::GetKind() const +{ + return mKind; +} + +bool BaseValue::SupportsConstant() const +{ + return false; +} + +void BaseValue::ReadFrom(std::istream& stream) +{ +} + +void BaseValue::WriteTo(std::ostream& stream) +{ +} + +BaseObjectValue::BaseObjectValue(Kind kind) + : BaseValue(kind) +{ + assert(kind >= KD_BaseObject && kind <= KD_BaseObjectLast); +} + +const BaseObjectDescription& BaseObjectValue::GetObjectDescription() const +{ + return QueryObjectInfo(this->GetKind()); +} diff --git a/app/source/Cplt/Model/Workflow/Value_RTTI.cpp b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp new file mode 100644 index 0000000..a2a6960 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp @@ -0,0 +1,174 @@ +#include "Value.hpp" + +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/Model/Workflow/Values/Database.hpp> +#include <Cplt/Model/Workflow/Values/Dictionary.hpp> +#include <Cplt/Model/Workflow/Values/List.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> + +constexpr BaseValue::KindInfo kEmptyInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(), +}; + +constexpr BaseValue::KindInfo kNumericInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(147, 226, 74), +}; + +constexpr BaseValue::KindInfo kTextInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(124, 21, 153), +}; + +constexpr BaseValue::KindInfo kDateTimeInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(147, 226, 74), +}; + +constexpr BaseValue::KindInfo kDatabaseRowIdInfo{ + .PinIcon = ImGui::IconType::Circle, + .PinColor = RgbaColor(216, 42, 221), +}; + +constexpr BaseValue::KindInfo kListInfo{ + .PinIcon = ImGui::IconType::Diamond, + .PinColor = RgbaColor(58, 154, 214), +}; + +constexpr BaseValue::KindInfo kDictionaryInfo{ + .PinIcon = ImGui::IconType::Diamond, + .PinColor = RgbaColor(240, 240, 34), +}; + +constexpr BaseValue::KindInfo kDatabaseRowInfo{ + .PinIcon = ImGui::IconType::Square, + .PinColor = RgbaColor(15, 124, 196), +}; + +constexpr BaseValue::KindInfo kObjectInfo{ + .PinIcon = ImGui::IconType::Square, + .PinColor = RgbaColor(161, 161, 161), +}; + +const BaseValue::KindInfo& BaseValue::QueryInfo(BaseValue::Kind kind) +{ + switch (kind) { + case KD_Numeric: return kNumericInfo; + case KD_Text: return kTextInfo; + case KD_DateTime: return kDateTimeInfo; + case KD_DatabaseRowId: return kDatabaseRowIdInfo; + case KD_List: return kListInfo; + case KD_Dictionary: return kDictionaryInfo; + + case KD_BaseObject: return kObjectInfo; + case KD_SaleDatabaseRow: + case KD_PurchaseDatabaseRow: + return kDatabaseRowInfo; + + case InvalidKind: break; + } + return kEmptyInfo; +} + +const char* BaseValue::Format(Kind kind) +{ + switch (kind) { + case KD_Numeric: return I18N_TEXT("Numeric", L10N_VALUE_NUMERIC); + case KD_Text: return I18N_TEXT("Text", L10N_VALUE_TEXT); + case KD_DateTime: return I18N_TEXT("Date/time", L10N_VALUE_DATE_TIME); + case KD_DatabaseRowId: return I18N_TEXT("Row id", L10N_VALUE_ROW_ID); + case KD_List: return I18N_TEXT("List", L10N_VALUE_LIST); + case KD_Dictionary: return I18N_TEXT("Dictionary", L10N_VALUE_DICT); + + case KD_BaseObject: return I18N_TEXT("Object", L10N_VALUE_OBJECT); + case KD_SaleDatabaseRow: return I18N_TEXT("Sale record", L10N_VALUE_SALE_RECORD); + case KD_PurchaseDatabaseRow: return I18N_TEXT("Purchase record", L10N_VALUE_PURCHASE_RECORD); + + case InvalidKind: break; + } + return ""; +} + +std::unique_ptr<BaseValue> BaseValue::CreateByKind(BaseValue::Kind kind) +{ + switch (kind) { + case KD_Numeric: return std::make_unique<NumericValue>(); + case KD_Text: return std::make_unique<TextValue>(); + case KD_DateTime: return std::make_unique<DateTimeValue>(); + case KD_DatabaseRowId: return std::make_unique<DatabaseRowIdValue>(); + case KD_List: return std::make_unique<ListValue>(); + case KD_Dictionary: return std::make_unique<DictionaryValue>(); + + case KD_BaseObject: return nullptr; + case KD_SaleDatabaseRow: return std::make_unique<SaleDatabaseRowValue>(); + case KD_PurchaseDatabaseRow: return std::make_unique<PurchaseDatabaseRowValue>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool BaseValue::IsInstance(const BaseValue* value) +{ + return true; +} + +const BaseObjectDescription kEmptyObjectInfo{ + .Properties = {}, +}; + +const BaseObjectDescription kSaleDbRowObject{ + .Properties = { + { + .Name = I18N_TEXT("Customer", L10N_VALUE_PROPERTY_CUSTOMER), + .Kind = BaseValue::KD_Text, + .Mutatable = false, + }, + { + .Name = I18N_TEXT("Deadline", L10N_VALUE_PROPERTY_DEADLINE), + .Kind = BaseValue::KD_DateTime, + }, + { + .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME), + .Kind = BaseValue::KD_DateTime, + }, + }, +}; + +const BaseObjectDescription kPurchaseDbRowObject{ + .Properties = { + { + .Name = I18N_TEXT("Factory", L10N_VALUE_PROPERTY_FACTORY), + .Kind = BaseValue::KD_Text, + .Mutatable = false, + }, + { + .Name = I18N_TEXT("Order time", L10N_VALUE_PROPERTY_ORDER_TIME), + .Kind = BaseValue::KD_DateTime, + }, + { + .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME), + .Kind = BaseValue::KD_DateTime, + }, + }, +}; + +const BaseObjectDescription& BaseObjectValue::QueryObjectInfo(Kind kind) +{ + switch (kind) { + case KD_BaseObject: return kEmptyObjectInfo; + case KD_SaleDatabaseRow: return kSaleDbRowObject; + case KD_PurchaseDatabaseRow: return kPurchaseDbRowObject; + + default: break; + } + return kEmptyObjectInfo; +} + +bool BaseObjectValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() >= KD_BaseObject && + value->GetKind() <= KD_BaseObjectLast; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.cpp b/app/source/Cplt/Model/Workflow/Values/Basic.cpp new file mode 100644 index 0000000..198387c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Basic.cpp @@ -0,0 +1,111 @@ +#include "Basic.hpp" + +#include <charconv> +#include <cmath> +#include <limits> + +bool NumericValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Numeric; +} + +NumericValue::NumericValue() + : BaseValue(BaseValue::KD_Numeric) +{ +} + +template <class T, int kMaxSize> +static std::string NumberToString(T value) +{ + char buf[kMaxSize]; + auto res = std::to_chars(buf, buf + kMaxSize, value); + if (res.ec == std::errc()) { + return std::string(buf, res.ptr); + } else { + return "<err>"; + } +} + +std::string NumericValue::GetTruncatedString() const +{ + constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10; + return ::NumberToString<int64_t, kMaxSize>((int64_t)mValue); +} + +std::string NumericValue::GetRoundedString() const +{ + constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10; + return ::NumberToString<int64_t, kMaxSize>((int64_t)std::round(mValue)); +} + +std::string NumericValue::GetString() const +{ + constexpr auto kMaxSize = std::numeric_limits<double>::max_digits10; + return ::NumberToString<double, kMaxSize>(mValue); +} + +int64_t NumericValue::GetInt() const +{ + return static_cast<int64_t>(mValue); +} + +double NumericValue::GetValue() const +{ + return mValue; +} + +void NumericValue::SetValue(double value) +{ + mValue = value; +} + +bool TextValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Text; +} + +TextValue::TextValue() + : BaseValue(BaseValue::KD_Text) +{ +} + +const std::string& TextValue::GetValue() const +{ + return mValue; +} + +void TextValue::SetValue(const std::string& value) +{ + mValue = value; +} + +bool DateTimeValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_DateTime; +} + +DateTimeValue::DateTimeValue() + : BaseValue(BaseValue::KD_DateTime) +{ +} + +std::string DateTimeValue::GetString() const +{ + namespace chrono = std::chrono; + auto t = chrono::system_clock::to_time_t(mValue); + + char data[32]; + std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); + + return std::string(data); +} + +const std::chrono::time_point<std::chrono::system_clock>& DateTimeValue::GetValue() const +{ + return mValue; +} + +void DateTimeValue::SetValue(const std::chrono::time_point<std::chrono::system_clock>& value) +{ + mValue = value; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.hpp b/app/source/Cplt/Model/Workflow/Values/Basic.hpp new file mode 100644 index 0000000..820fb13 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Basic.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <chrono> +#include <cstdint> +#include <string> + +class NumericValue : public BaseValue +{ +private: + double mValue; + +public: + static bool IsInstance(const BaseValue* value); + NumericValue(); + + NumericValue(const NumericValue&) = delete; + NumericValue& operator=(const NumericValue&) = delete; + NumericValue(NumericValue&&) = default; + NumericValue& operator=(NumericValue&&) = default; + + std::string GetTruncatedString() const; + std::string GetRoundedString() const; + std::string GetString() const; + + int64_t GetInt() const; + double GetValue() const; + void SetValue(double value); +}; + +class TextValue : public BaseValue +{ +private: + std::string mValue; + +public: + static bool IsInstance(const BaseValue* value); + TextValue(); + + TextValue(const TextValue&) = delete; + TextValue& operator=(const TextValue&) = delete; + TextValue(TextValue&&) = default; + TextValue& operator=(TextValue&&) = default; + + const std::string& GetValue() const; + void SetValue(const std::string& value); +}; + +class DateTimeValue : public BaseValue +{ +private: + std::chrono::time_point<std::chrono::system_clock> mValue; + +public: + static bool IsInstance(const BaseValue* value); + DateTimeValue(); + + DateTimeValue(const DateTimeValue&) = delete; + DateTimeValue& operator=(const DateTimeValue&) = delete; + DateTimeValue(DateTimeValue&&) = default; + DateTimeValue& operator=(DateTimeValue&&) = default; + + std::string GetString() const; + const std::chrono::time_point<std::chrono::system_clock>& GetValue() const; + void SetValue(const std::chrono::time_point<std::chrono::system_clock>& value); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/Database.cpp b/app/source/Cplt/Model/Workflow/Values/Database.cpp new file mode 100644 index 0000000..25b77e9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Database.cpp @@ -0,0 +1,88 @@ +#include "Database.hpp" + +#include <Cplt/Model/Database.hpp> +#include <Cplt/Model/Workflow/ValueInternals.hpp> + +#include <limits> + +TableKind DatabaseRowIdValue::GetTable() const +{ + return mTable; +} + +int64_t DatabaseRowIdValue::GetRowId() const +{ + return mRowId; +} + +bool DatabaseRowIdValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_DatabaseRowId; +} + +DatabaseRowIdValue::DatabaseRowIdValue() + : BaseValue(KD_DatabaseRowId) + , mTable{ TableKind::Sales } + , mRowId{ std::numeric_limits<int64_t>::max() } +{ +} + +bool SaleDatabaseRowValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_SaleDatabaseRow; +} + +SaleDatabaseRowValue::SaleDatabaseRowValue() + : BaseObjectValue(KD_SaleDatabaseRow) +{ +} + +const BaseValue* SaleDatabaseRowValue::GetProperty(int idx) const +{ + switch (idx) { + case 0: return &mCustomerName; + case 1: return &mDeadline; + case 2: return &mDeliveryTime; + default: return nullptr; + } +} + +bool SaleDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value) +{ + switch (idx) { + case 0: return false; + case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeadline, value.get()); break; + case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break; + } + return true; +} + +bool PurchaseDatabaseRowValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_PurchaseDatabaseRow; +} + +PurchaseDatabaseRowValue::PurchaseDatabaseRowValue() + : BaseObjectValue(KD_PurchaseDatabaseRow) +{ +} + +const BaseValue* PurchaseDatabaseRowValue::GetProperty(int idx) const +{ + switch (idx) { + case 0: return &mFactoryName; + case 1: return &mOrderTime; + case 2: return &mDeliveryTime; + default: return nullptr; + } +} + +bool PurchaseDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value) +{ + switch (idx) { + case 0: return false; + case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mOrderTime, value.get()); break; + case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break; + } + return true; +} diff --git a/app/source/Cplt/Model/Workflow/Values/Database.hpp b/app/source/Cplt/Model/Workflow/Values/Database.hpp new file mode 100644 index 0000000..f1c1571 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Database.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> +#include <Cplt/Model/Workflow/Values/Basic.hpp> +#include <Cplt/fwd.hpp> + +class DatabaseRowIdValue : public BaseValue +{ +private: + TableKind mTable; + int64_t mRowId; + +public: + static bool IsInstance(const BaseValue* value); + DatabaseRowIdValue(); + + TableKind GetTable() const; + int64_t GetRowId() const; +}; + +class SaleDatabaseRowValue : public BaseObjectValue +{ +private: + int mCustomerId; + TextValue mCustomerName; + DateTimeValue mDeadline; + DateTimeValue mDeliveryTime; + +public: + static bool IsInstance(const BaseValue* value); + SaleDatabaseRowValue(); + + virtual const BaseValue* GetProperty(int idx) const; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value); +}; + +class PurchaseDatabaseRowValue : public BaseObjectValue +{ +private: + int mFactoryId; + TextValue mFactoryName; + DateTimeValue mOrderTime; + DateTimeValue mDeliveryTime; + +public: + static bool IsInstance(const BaseValue* value); + PurchaseDatabaseRowValue(); + + virtual const BaseValue* GetProperty(int idx) const; + virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp new file mode 100644 index 0000000..97bf509 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp @@ -0,0 +1,49 @@ +#include "Dictionary.hpp" + +#include <Cplt/Utils/Macros.hpp> + +bool DictionaryValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_Dictionary; +} + +DictionaryValue::DictionaryValue() + : BaseValue(KD_Dictionary) +{ +} + +int DictionaryValue::GetCount() const +{ + return mElements.size(); +} + +BaseValue* DictionaryValue::Find(std::string_view key) +{ + auto iter = mElements.find(key); + if (iter != mElements.end()) { + return iter.value().get(); + } else { + return nullptr; + } +} + +BaseValue* DictionaryValue::Insert(std::string_view key, std::unique_ptr<BaseValue>& value) +{ + auto [iter, success] = mElements.insert(key, std::move(value)); + if (success) { + return iter.value().get(); + } else { + return nullptr; + } +} + +BaseValue& DictionaryValue::InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value) +{ + auto [iter, DISCARD] = mElements.emplace(key, std::move(value)); + return *iter.value(); +} + +void DictionaryValue::Remove(std::string_view key) +{ + mElements.erase(mElements.find(key)); +} diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp new file mode 100644 index 0000000..6eff308 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <tsl/array_map.h> +#include <memory> +#include <string> +#include <string_view> + +class DictionaryValue : public BaseValue +{ +private: + tsl::array_map<char, std::unique_ptr<BaseValue>> mElements; + +public: + static bool IsInstance(const BaseValue* value); + DictionaryValue(); + + int GetCount() const; + BaseValue* Find(std::string_view key); + + BaseValue* Insert(std::string_view key, std::unique_ptr<BaseValue>& value); + BaseValue& InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value); + void Remove(std::string_view key); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/List.cpp b/app/source/Cplt/Model/Workflow/Values/List.cpp new file mode 100644 index 0000000..9fd6bfd --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/List.cpp @@ -0,0 +1,100 @@ +#include "List.hpp" + +#include <utility> + +BaseValue* ListValue::Iterator::operator*() const +{ + return mIter->get(); +} + +BaseValue* ListValue::Iterator::operator->() const +{ + return mIter->get(); +} + +ListValue::Iterator& ListValue::Iterator::operator++() +{ + ++mIter; + return *this; +} + +ListValue::Iterator ListValue::Iterator::operator++(int) const +{ + return Iterator(mIter + 1); +} + +ListValue::Iterator& ListValue::Iterator::operator--() +{ + --mIter; + return *this; +} + +ListValue::Iterator ListValue::Iterator::operator--(int) const +{ + return Iterator(mIter - 1); +} + +bool operator==(const ListValue::Iterator& a, const ListValue::Iterator& b) +{ + return a.mIter == b.mIter; +} + +ListValue::Iterator::Iterator(decltype(mIter) iter) + : mIter{ iter } +{ +} + +bool ListValue::IsInstance(const BaseValue* value) +{ + return value->GetKind() == KD_List; +} + +ListValue::ListValue() + : BaseValue(KD_List) +{ +} + +int ListValue::GetCount() const +{ + return mElements.size(); +} + +BaseValue* ListValue::GetElement(int i) const +{ + return mElements[i].get(); +} + +void ListValue::Append(std::unique_ptr<BaseValue> element) +{ + mElements.push_back(std::move(element)); +} + +void ListValue::Insert(int i, std::unique_ptr<BaseValue> element) +{ + mElements.insert(mElements.begin() + i, std::move(element)); +} + +void ListValue::Insert(Iterator iter, std::unique_ptr<BaseValue> element) +{ + mElements.insert(iter.mIter, std::move(element)); +} + +void ListValue::Remove(int i) +{ + mElements.erase(mElements.begin() + i); +} + +void ListValue::Remove(Iterator iter) +{ + mElements.erase(iter.mIter); +} + +ListValue::Iterator ListValue::begin() +{ + return Iterator(mElements.begin()); +} + +ListValue::Iterator ListValue::end() +{ + return Iterator(mElements.end()); +} diff --git a/app/source/Cplt/Model/Workflow/Values/List.hpp b/app/source/Cplt/Model/Workflow/Values/List.hpp new file mode 100644 index 0000000..cc8e061 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/List.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include <Cplt/Model/Workflow/Value.hpp> + +#include <memory> +#include <vector> + +class ListValue : public BaseValue +{ +public: + class Iterator + { + private: + std::vector<std::unique_ptr<BaseValue>>::iterator mIter; + + public: + BaseValue* operator*() const; + BaseValue* operator->() const; + + Iterator& operator++(); + Iterator operator++(int) const; + Iterator& operator--(); + Iterator operator--(int) const; + + friend bool operator==(const Iterator& a, const Iterator& b); + + private: + friend class ListValue; + Iterator(decltype(mIter) iter); + }; + +private: + std::vector<std::unique_ptr<BaseValue>> mElements; + +public: + static bool IsInstance(const BaseValue* value); + ListValue(); + + int GetCount() const; + BaseValue* GetElement(int i) const; + + void Append(std::unique_ptr<BaseValue> element); + void Insert(int i, std::unique_ptr<BaseValue> element); + void Insert(Iterator iter, std::unique_ptr<BaseValue> element); + void Remove(int i); + void Remove(Iterator iter); + + Iterator begin(); + Iterator end(); +}; diff --git a/app/source/Cplt/Model/Workflow/Values/fwd.hpp b/app/source/Cplt/Model/Workflow/Values/fwd.hpp new file mode 100644 index 0000000..51a04e9 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Values/fwd.hpp @@ -0,0 +1,17 @@ +#pragma once + +// Basic.hpp +class NumericValue; +class TextValue; +class DateTimeValue; + +// Database.hpp +class DatabaseRowIdValue; +class SaleDatabaseRowValue; +class PurchaseDatabaseRowValue; + +// Dictionary.hpp +class DictionaryValue; + +// List.hpp +class ListValue; diff --git a/app/source/Cplt/Model/Workflow/Workflow.hpp b/app/source/Cplt/Model/Workflow/Workflow.hpp new file mode 100644 index 0000000..e075e3c --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow.hpp @@ -0,0 +1,316 @@ +#pragma once + +#include <Cplt/Model/Assets.hpp> +#include <Cplt/Model/Workflow/Value.hpp> +#include <Cplt/Utils/Vector.hpp> +#include <Cplt/fwd.hpp> + +#include <imgui_node_editor.h> +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <functional> +#include <iosfwd> +#include <limits> +#include <memory> +#include <span> +#include <string> +#include <variant> +#include <vector> + +namespace ImNodes = ax::NodeEditor; + +class WorkflowConnection +{ +public: + static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max(); + + uint32_t Id; + uint32_t SourceNode; + uint32_t SourcePin; + uint32_t DestinationNode; + uint32_t DestinationPin; + +public: + WorkflowConnection(); + + bool IsValid() const; + + /// Used for `LinkId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading). + ImNodes::LinkId GetLinkId() const; + + void DrawDebugInfo() const; + void ReadFrom(std::istream& stream); + void WriteTo(std::ostream& stream) const; +}; + +class WorkflowNode +{ +public: + static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max(); + static constexpr auto kInvalidPinId = std::numeric_limits<uint32_t>::max(); + + enum Type + { + InputType, + TransformType, + OutputType, + }; + + enum Kind + { + KD_NumericAddition, + KD_NumericSubtraction, + KD_NumericMultiplication, + KD_NumericDivision, + KD_NumericExpression, + KD_TextFormatting, + KD_DocumentTemplateExpansion, + KD_FormInput, + KD_DatabaseRowsInput, + + InvalidKind, + KindCount = InvalidKind, + }; + + enum Category + { + CG_Numeric, + CG_Text, + CG_Document, + CG_UserInput, + CG_SystemInput, + CG_Output, + + InvalidCategory, + CategoryCount = InvalidCategory, + }; + + struct InputPin + { + uint32_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::InvalidKind; + bool ConnectionToConst = false; + + /// A constant connection connects from a user-specified constant value, feeding to a valid \c DestinationNode and \c DestinationPin (i.e. input pins). + bool IsConstantConnection() const; + bool IsConnected() const; + BaseValue::Kind GetMatchingType() const; + }; + + struct OutputPin + { + uint32_t Connection = WorkflowConnection::kInvalidId; + BaseValue::Kind MatchingType = BaseValue::InvalidKind; + + bool IsConnected() const; + BaseValue::Kind GetMatchingType() const; + }; + +protected: + friend class Workflow; + friend class WorkflowEvaluationContext; + + Workflow* mWorkflow; + std::vector<InputPin> mInputs; + std::vector<OutputPin> mOutputs; + Vec2i mPosition; + uint32_t mId; + Kind mKind; + int mDepth; + bool mLocked; + +public: + static const char* FormatKind(Kind kind); + static const char* FormatCategory(Category category); + static const char* FormatType(Type type); + static Category QueryCategory(Kind kind); + static std::span<const Kind> QueryCategoryMembers(Category category); + static std::unique_ptr<WorkflowNode> CreateByKind(Kind kind); + + static bool IsInstance(const WorkflowNode* node); + + WorkflowNode(Kind kind, bool locked); + virtual ~WorkflowNode() = default; + + WorkflowNode(const WorkflowNode&) = delete; + WorkflowNode& operator=(const WorkflowNode&) = delete; + WorkflowNode(WorkflowNode&&) = default; + WorkflowNode& operator=(WorkflowNode&&) = default; + + void SetPosition(const Vec2i& position); + Vec2i GetPosition() const; + + uint32_t GetId() const; + /// Used for `NodeId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading). + ImNodes::NodeId GetNodeId() const; + Kind GetKind() const; + int GetDepth() const; + bool IsLocked() const; + + Type GetType() const; + bool IsInputNode() const; + bool IsOutputNode() const; + + void ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId); + void DisconnectInput(uint32_t pinId); + + void DrawInputPinDebugInfo(uint32_t pinId) const; + const InputPin& GetInputPin(uint32_t pinId) const; + ImNodes::PinId GetInputPinUniqueId(uint32_t pinId) const; + + void ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId); + void DisconnectOutput(uint32_t pinId); + + void DrawOutputPinDebugInfo(uint32_t pinId) const; + const OutputPin& GetOutputPin(uint32_t pinId) const; + ImNodes::PinId GetOutputPinUniqueId(uint32_t pinId) const; + + virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0; + + void Draw(); + virtual void DrawExtra() {} + + void DrawDebugInfo() const; + virtual void DrawExtraDebugInfo() const {} + + virtual void ReadFrom(std::istream& istream); + virtual void WriteTo(std::ostream& ostream); + +protected: + InputPin& InsertInputPin(int atIdx); + void RemoveInputPin(int pin); + void SwapInputPin(int a, int b); + OutputPin& InsertOutputPin(int atIdx); + void RemoveOutputPin(int pin); + void SwapOutputPin(int a, int b); + + /* For \c Workflow to invoke, override by implementations */ + + void OnAttach(Workflow& workflow, uint32_t newId); + void OnDetach(); +}; + +class Workflow : public Asset +{ + friend class WorkflowNode; + friend class WorkflowEvaluationContext; + class Private; + +public: + using CategoryType = WorkflowAssetList; + static constinit const WorkflowAssetList Category; + +private: + std::vector<WorkflowConnection> mConnections; + std::vector<std::unique_ptr<WorkflowNode>> mNodes; + std::vector<std::unique_ptr<BaseValue>> mConstants; + std::vector<std::vector<uint32_t>> mDepthGroups; + int mConnectionCount; + int mNodeCount; + int mConstantCount; + bool mDepthsDirty = true; + +public: + /* Graph access */ + + const std::vector<WorkflowConnection>& GetConnections() const; + std::vector<WorkflowConnection>& GetConnections(); + const std::vector<std::unique_ptr<WorkflowNode>>& GetNodes() const; + std::vector<std::unique_ptr<WorkflowNode>>& GetNodes(); + const std::vector<std::unique_ptr<BaseValue>>& GetConstants() const; + std::vector<std::unique_ptr<BaseValue>>& GetConstants(); + + WorkflowConnection* GetConnectionById(uint32_t id); + WorkflowConnection* GetConnectionByLinkId(ImNodes::LinkId linkId); + WorkflowNode* GetNodeById(uint32_t id); + WorkflowNode* GetNodeByNodeId(ImNodes::NodeId nodeId); + BaseValue* GetConstantById(uint32_t id); + + struct GlobalPinId + { + WorkflowNode* Node; + uint32_t PinId; + /// true => input pin + /// false => output pin + bool IsOutput; + }; + + /// `pinId` should be the `UniqueId` of a pin from a node that's within this workflow. + GlobalPinId DisassembleGlobalPinId(ImNodes::PinId id); + ImNodes::PinId FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const; + + const std::vector<std::vector<uint32_t>>& GetDepthGroups() const; + bool DoesDepthNeedsUpdate() const; + + /* Graph mutation */ + + void AddNode(std::unique_ptr<WorkflowNode> step); + void RemoveNode(uint32_t id); + + void RemoveConnection(uint32_t id); + + bool Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin); + bool DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin); + bool DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin); + + /* Graph rebuild */ + + enum GraphUpdateResult + { + /// Successfully rebuilt graph dependent data. + /// Details: nothing is written. + GUR_Success, + /// Nothing has changed since last time UpdateGraph() was called. + /// Details: nothing is written. + GUR_NoWorkToDo, + /// Details: list of nodes is written. + GUR_UnsatisfiedDependencies, + /// Details: list of nodes is written. + GUR_UnreachableNodes, + }; + + using GraphUpdateDetails = std::variant< + // Case: nothing + std::monostate, + // Case: list of nodes (ids) + std::vector<uint32_t>>; + + GraphUpdateResult UpdateGraph(GraphUpdateDetails* details = nullptr); + + /* Serialization */ + + void ReadFromDataStream(InputDataStream& stream); + void WriteToDataStream(OutputDataStream& stream) const; + +private: + std::pair<WorkflowConnection&, uint32_t> AllocWorkflowConnection(); + std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> AllocWorkflowStep(); +}; + +class WorkflowAssetList final : public AssetListTyped<Workflow> +{ +private: + // AC = Asset Creator + std::string mACNewName; + NameSelectionError mACNewNameError = NameSelectionError::Empty; + +public: + // Inherit constructors + using AssetListTyped::AssetListTyped; + +protected: + void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override; + + std::string RetrieveNameFromFile(const std::filesystem::path& file) const override; + uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override; + std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override; + + bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override; + Workflow* LoadInstance(const SavedAsset& assetInfo) const override; + Workflow* CreateInstance(const SavedAsset& assetInfo) const override; + bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override; + + void DisplayAssetCreator(ListState& state) override; + void DisplayDetailsTable(ListState& state) const override; +}; diff --git a/app/source/Cplt/Model/Workflow/Workflow_Main.cpp b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp new file mode 100644 index 0000000..0f35b32 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp @@ -0,0 +1,846 @@ +#include "Workflow.hpp" + +#include <Cplt/Model/GlobalStates.hpp> +#include <Cplt/Model/Project.hpp> +#include <Cplt/UI/UI.hpp> +#include <Cplt/Utils/I18n.hpp> +#include <Cplt/Utils/IO/Archive.hpp> +#include <Cplt/Utils/UUID.hpp> + +#include <imgui.h> +#include <imgui_node_editor.h> +#include <imgui_stdlib.h> +#include <tsl/robin_set.h> +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <fstream> +#include <iostream> +#include <queue> +#include <utility> + +using namespace std::literals::string_view_literals; +namespace fs = std::filesystem; +namespace ImNodes = ax::NodeEditor; + +WorkflowConnection::WorkflowConnection() + : Id{ 0 } + , SourceNode{ WorkflowNode::kInvalidId } + , SourcePin{ WorkflowNode::kInvalidPinId } + , DestinationNode{ WorkflowNode::kInvalidId } + , DestinationPin{ WorkflowNode::kInvalidPinId } +{ +} + +bool WorkflowConnection::IsValid() const +{ + return Id != 0; +} + +ImNodes::LinkId WorkflowConnection::GetLinkId() const +{ + // Our id is 0-based (represents an index directly) + // but imgui-node-editor uses the value 0 to represent a null id, so we need to offset by 1 + return Id + 1; +} + +void WorkflowConnection::DrawDebugInfo() const +{ + ImGui::Text("Source (node with output pin):"); + ImGui::Text("{ Node = %u, Pin = %u }", SourceNode, SourcePin); + ImGui::Text("Destination (node with input pin):"); + ImGui::Text("{ Node = %u, Pin = %u }", DestinationNode, DestinationPin); +} + +void WorkflowConnection::ReadFrom(std::istream& stream) +{ + stream >> SourceNode >> SourcePin; + stream >> DestinationNode >> DestinationPin; +} + +void WorkflowConnection::WriteTo(std::ostream& stream) const +{ + stream << SourceNode << SourcePin; + stream << DestinationNode << DestinationPin; +} + +bool WorkflowNode::InputPin::IsConstantConnection() const +{ + return ConnectionToConst && IsConnected(); +} + +bool WorkflowNode::InputPin::IsConnected() const +{ + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const +{ + return MatchingType; +} + +bool WorkflowNode::OutputPin::IsConnected() const +{ + return Connection != WorkflowConnection::kInvalidId; +} + +BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const +{ + return MatchingType; +} + +WorkflowNode::WorkflowNode(Kind kind, bool locked) + : mKind{ kind } + , mDepth{ -1 } + , mLocked(locked) +{ +} + +Vec2i WorkflowNode::GetPosition() const +{ + return mPosition; +} + +void WorkflowNode::SetPosition(const Vec2i& position) +{ + mPosition = position; +} + +uint32_t WorkflowNode::GetId() const +{ + return mId; +} + +ImNodes::NodeId WorkflowNode::GetNodeId() const +{ + // See WorkflowConnection::GetLinkId for the rationale + return mId + 1; +} + +WorkflowNode::Kind WorkflowNode::GetKind() const +{ + return mKind; +} + +int WorkflowNode::GetDepth() const +{ + return mDepth; +} + +bool WorkflowNode::IsLocked() const +{ + return mLocked; +} + +WorkflowNode::Type WorkflowNode::GetType() const +{ + if (IsInputNode()) { + return InputType; + } else if (IsOutputNode()) { + return OutputType; + } else { + return TransformType; + } +} + +bool WorkflowNode::IsInputNode() const +{ + return mInputs.size() == 0; +} + +bool WorkflowNode::IsOutputNode() const +{ + return mOutputs.size() == 0; +} + +void WorkflowNode::ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId) +{ + mWorkflow->Connect(*this, pinId, srcNode, srcPinId); +} + +void WorkflowNode::DisconnectInput(uint32_t pinId) +{ + mWorkflow->DisconnectByDestination(*this, pinId); +} + +void WorkflowNode::DrawInputPinDebugInfo(uint32_t pinId) const +{ + ImGui::Text("Node ID: %d", mId); + ImGui::Text("Pin ID: (input) %d", pinId); +} + +const WorkflowNode::InputPin& WorkflowNode::GetInputPin(uint32_t pinId) const +{ + return mInputs[pinId]; +} + +ImNodes::PinId WorkflowNode::GetInputPinUniqueId(uint32_t pinId) const +{ + return mWorkflow->FabricateGlobalPinId(*this, pinId, false); +} + +void WorkflowNode::ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId) +{ + mWorkflow->Connect(dstNode, dstPinId, *this, pinId); +} + +void WorkflowNode::DisconnectOutput(uint32_t pinId) +{ + mWorkflow->DisconnectBySource(*this, pinId); +} + +void WorkflowNode::DrawOutputPinDebugInfo(uint32_t pinId) const +{ + ImGui::Text("Node ID: %d", mId); + ImGui::Text("Pin ID: (output) %d", pinId); +} + +const WorkflowNode::OutputPin& WorkflowNode::GetOutputPin(uint32_t pinId) const +{ + return mOutputs[pinId]; +} + +ImNodes::PinId WorkflowNode::GetOutputPinUniqueId(uint32_t pinId) const +{ + return mWorkflow->FabricateGlobalPinId(*this, pinId, true); +} + +void WorkflowNode::Draw() +{ + for (uint32_t i = 0; i < mInputs.size(); ++i) { + auto& pin = mInputs[i]; + auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType); + ImNodes::BeginPin(GetInputPinUniqueId(i), ImNodes::PinKind::Input); + // TODO + ImNodes::EndPin(); + } + for (uint32_t i = 0; i < mOutputs.size(); ++i) { + auto& pin = mOutputs[i]; + auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType); + ImNodes::BeginPin(GetOutputPinUniqueId(i), ImNodes::PinKind::Output); + // TODO + ImNodes::EndPin(); + } +} + +void WorkflowNode::DrawDebugInfo() const +{ + ImGui::Text("Node kind: %s", FormatKind(mKind)); + ImGui::Text("Node type: %s", FormatType(GetType())); + ImGui::Text("Node ID: %u", mId); + ImGui::Text("Depth: %d", mDepth); + DrawExtraDebugInfo(); +} + +void WorkflowNode::ReadFrom(std::istream& stream) +{ + stream >> mId; + stream >> mPosition.x >> mPosition.y; +} + +void WorkflowNode::WriteTo(std::ostream& stream) +{ + stream << mId; + stream << mPosition.x << mPosition.y; +} + +WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx) +{ + assert(atIdx >= 0 && atIdx < mInputs.size()); + + mInputs.push_back(InputPin{}); + for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapInputPin(i, i + 1); + } + + return mInputs[atIdx]; +} + +void WorkflowNode::RemoveInputPin(int pin) +{ + DisconnectInput(pin); + for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mInputs.resize(mInputs.size() - 1); +} + +void WorkflowNode::SwapInputPin(int a, int b) +{ + auto& pinA = mInputs[a]; + auto& pinB = mInputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected() && !pinA.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.DestinationPin = b; + } + if (pinB.IsConnected() && !pinB.IsConstantConnection()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.DestinationPin = a; + } + } + + std::swap(pinA, pinB); +} + +WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx) +{ + assert(atIdx >= 0 && atIdx < mOutputs.size()); + + mOutputs.push_back(OutputPin{}); + for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) { + SwapOutputPin(i, i + 1); + } + + return mOutputs[atIdx]; +} + +void WorkflowNode::RemoveOutputPin(int pin) +{ + DisconnectOutput(pin); + for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) { + SwapInputPin(i, i + 1); + } + mOutputs.resize(mOutputs.size() - 1); +} + +void WorkflowNode::SwapOutputPin(int a, int b) +{ + auto& pinA = mOutputs[a]; + auto& pinB = mOutputs[b]; + + if (mWorkflow) { + if (pinA.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinA.Connection); + conn.SourcePin = b; + } + if (pinB.IsConnected()) { + auto& conn = *mWorkflow->GetConnectionById(pinB.Connection); + conn.SourcePin = a; + } + } + + std::swap(pinA, pinB); +} + +void WorkflowNode::OnAttach(Workflow& workflow, uint32_t newId) +{ +} + +void WorkflowNode::OnDetach() +{ +} + +const std::vector<WorkflowConnection>& Workflow::GetConnections() const +{ + return mConnections; +} + +std::vector<WorkflowConnection>& Workflow::GetConnections() +{ + return mConnections; +} + +const std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() const +{ + return mNodes; +} + +std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() +{ + return mNodes; +} + +const std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() const +{ + return mConstants; +} + +std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() +{ + return mConstants; +} + +WorkflowConnection* Workflow::GetConnectionById(uint32_t id) +{ + return &mConnections[id]; +} + +WorkflowConnection* Workflow::GetConnectionByLinkId(ImNodes::LinkId id) +{ + return &mConnections[(uint32_t)(size_t)id - 1]; +} + +WorkflowNode* Workflow::GetNodeById(uint32_t id) +{ + return mNodes[id].get(); +} + +WorkflowNode* Workflow::GetNodeByNodeId(ImNodes::NodeId id) +{ + return mNodes[(uint32_t)(size_t)id - 1].get(); +} + +BaseValue* Workflow::GetConstantById(uint32_t id) +{ + return mConstants[id].get(); +} + +Workflow::GlobalPinId Workflow::DisassembleGlobalPinId(ImNodes::PinId pinId) +{ + // imgui-node-editor requires all pins to have a global, unique id + // but in our model the pin are typed (input vs output) and associated with a node: there is no built-in global id + // Therefore we encode one ourselves + + // Global pin id format + // nnnnnnnn nnnnnnnn nnnnnnnn nnnnnnnn Tppppppp ppppppppp pppppppp pppppppp + // <------- (32 bits) node id -------> ^<------ (31 bits) pin id --------> + // | (1 bit) input (false) vs output (true) + + // 1 is added to pin id to prevent the 0th node's 0th input pin resulting in a 0 global pin id + // (this is problematic because imgui-node-editor use 0 to represent null) + + auto id = static_cast<uint64_t>(pinId); + GlobalPinId result; + + result.Node = mNodes[id >> 32].get(); + result.PinId = (uint32_t)(id & 0x000000001FFFFFFF) - 1; + result.IsOutput = id >> 31; + + return result; +} + +ImNodes::PinId Workflow::FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const +{ + // See this->DisassembleGlobalPinId for format details and rationale + + uint64_t id = 0; + id |= ((uint64_t)node.GetId() << 32); + id |= (isOutput << 31); + id |= ((pinId + 1) & 0x1FFFFFFF); + + return id; +} + +const std::vector<std::vector<uint32_t>>& Workflow::GetDepthGroups() const +{ + return mDepthGroups; +} + +bool Workflow::DoesDepthNeedsUpdate() const +{ + return mDepthsDirty; +} + +void Workflow::AddNode(std::unique_ptr<WorkflowNode> step) +{ + auto [storage, id] = AllocWorkflowStep(); + storage = std::move(step); + storage->OnAttach(*this, id); + storage->mWorkflow = this; + storage->mId = id; +} + +void Workflow::RemoveNode(uint32_t id) +{ + auto& step = mNodes[id]; + if (step == nullptr) return; + + step->OnDetach(); + step->mWorkflow = nullptr; + step->mId = WorkflowNode::kInvalidId; +} + +void Workflow::RemoveConnection(uint32_t id) +{ + auto& conn = mConnections[id]; + if (!conn.IsValid()) return; + + mNodes[conn.SourceNode]->mInputs[conn.SourcePin].Connection = WorkflowNode::kInvalidId; + mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin].Connection = WorkflowNode::kInvalidId; + + conn = {}; + mDepthsDirty = true; +} + +bool Workflow::Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin) +{ + auto& src = sourceNode.mOutputs[sourcePin]; + auto& dst = destinationNode.mInputs[destinationPin]; + + // TODO report error to user? + if (src.GetMatchingType() != dst.GetMatchingType()) { + return false; + } + + if (src.IsConnected()) { + DisconnectBySource(sourceNode, sourcePin); + } + + auto [conn, id] = AllocWorkflowConnection(); + conn.SourceNode = sourceNode.GetId(); + conn.SourcePin = sourcePin; + conn.DestinationNode = destinationNode.GetId(); + conn.DestinationPin = destinationPin; + + src.Connection = id; + dst.Connection = id; + + mDepthsDirty = true; + return true; +} + +bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin) +{ + auto& sn = sourceNode.mOutputs[sourcePin]; + if (!sn.IsConnected()) return false; + + auto& conn = mConnections[sn.Connection]; + auto& dn = mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; + + mDepthsDirty = true; + return true; +} + +bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin) +{ + auto& dn = destinationNode.mOutputs[destinationPin]; + if (!dn.IsConnected()) return false; + + auto& conn = mConnections[dn.Connection]; + auto& sn = mNodes[conn.SourceNode]->mInputs[conn.SourcePin]; + + sn.Connection = WorkflowConnection::kInvalidId; + dn.Connection = WorkflowConnection::kInvalidId; + conn = {}; + + mDepthsDirty = true; + return true; +} + +Workflow::GraphUpdateResult Workflow::UpdateGraph(GraphUpdateDetails* details) +{ + if (!mDepthsDirty) { + return GUR_NoWorkToDo; + } + + // Terminology: + // - Dependency = nodes its input pins are connected to + // - Dependents = nodes its output pins are connected to + + struct WorkingNode + { + // The max depth out of all dependency nodes, maintained during the traversal and committed as the actual depth + // when all dependencies of this node has been resolved. Add 1 to get the depth that will be assigned to the node. + int MaximumDepth = 0; + int FulfilledInputCount = 0; + }; + + std::vector<WorkingNode> workingNodes; + std::queue<uint32_t> q; + + // Check if all dependencies of this node is satisfied + auto CheckNodeDependencies = [&](WorkflowNode& node) -> bool { + for (auto& pin : node.mInputs) { + if (!pin.IsConnected()) { + return false; + } + } + return true; + }; + + workingNodes.reserve(mNodes.size()); + { + std::vector<uint32_t> unsatisfiedNodes; + for (uint32_t i = 0; i < mNodes.size(); ++i) { + auto& node = mNodes[i]; + workingNodes.push_back(WorkingNode{}); + + if (!node) continue; + + if (!CheckNodeDependencies(*node)) { + unsatisfiedNodes.push_back(i); + } + + node->mDepth = -1; + + // Start traversing with the input nodes + if (node->GetType() == WorkflowNode::InputType) { + q.push(i); + } + } + + if (!unsatisfiedNodes.empty()) { + if (details) { + details->emplace<decltype(unsatisfiedNodes)>(std::move(unsatisfiedNodes)); + } + return GUR_UnsatisfiedDependencies; + } + } + + auto ProcessNode = [&](WorkflowNode& node) -> void { + for (auto& pin : node.mOutputs) { + if (!pin.IsConnected()) continue; + auto& conn = mConnections[pin.Connection]; + + auto& wn = workingNodes[conn.DestinationNode]; + auto& n = *mNodes[conn.DestinationPin].get(); + + wn.FulfilledInputCount++; + wn.MaximumDepth = std::max(node.mDepth, wn.MaximumDepth); + + // Node's dependency is fulfilled, we can process its dependents next + // We use >= here because for a many-to-one pin, the dependency is an "or" relation ship, i.e. any of the nodes firing before this will fulfill the requirement + if (n.mInputs.size() >= wn.FulfilledInputCount) { + n.mDepth = wn.MaximumDepth + 1; + } + } + }; + + int processedNodes = 0; + while (!q.empty()) { + auto& wn = workingNodes[q.front()]; + auto& n = *mNodes[q.front()]; + q.pop(); + processedNodes++; + + ProcessNode(n); + } + + if (processedNodes < mNodes.size()) { + // There is unreachable nodes, collect them and report to the caller + + std::vector<uint32_t> unreachableNodes; + for (uint32_t i = 0; i < mNodes.size(); ++i) { + auto& wn = workingNodes[i]; + auto& n = *mNodes[i]; + + // This is a reachable node + if (n.mDepth != -1) continue; + + unreachableNodes.push_back(i); + } + + if (details) { + details->emplace<decltype(unreachableNodes)>(std::move(unreachableNodes)); + } + return GUR_UnreachableNodes; + } + + return GUR_Success; +} + +class Workflow::Private +{ +public: + template <class TSelf, class TProxy> + static void OperateStream(TSelf& self, TProxy& proxy) + { + // TODO + } +}; + +void Workflow::ReadFromDataStream(InputDataStream& stream) +{ + Private::OperateStream(*this, stream); +} + +void Workflow::WriteToDataStream(OutputDataStream& stream) const +{ + Private::OperateStream(*this, stream); +} + +std::pair<WorkflowConnection&, uint32_t> Workflow::AllocWorkflowConnection() +{ + for (size_t idx = 0; idx < mConnections.size(); ++idx) { + auto& elm = mConnections[idx]; + if (!elm.IsValid()) { + return { elm, (uint32_t)idx }; + } + } + + auto id = (uint32_t)mConnections.size(); + auto& conn = mConnections.emplace_back(WorkflowConnection{}); + conn.Id = id; + + return { conn, id }; +} + +std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> Workflow::AllocWorkflowStep() +{ + for (size_t idx = 0; idx < mNodes.size(); ++idx) { + auto& elm = mNodes[idx]; + if (elm == nullptr) { + return { elm, (uint32_t)idx }; + } + } + + auto id = (uint32_t)mNodes.size(); + auto& node = mNodes.emplace_back(std::unique_ptr<WorkflowNode>()); + + return { node, id }; +} + +void WorkflowAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const +{ + auto dir = GetConnectedProject().GetWorkflowsDirectory(); + DiscoverFilesByExtension(callback, dir, ".cplt-workflow"sv); +} + +std::string WorkflowAssetList::RetrieveNameFromFile(const fs::path& file) const +{ + auto res = DataArchive::LoadFile(file); + if (!res) return ""; + auto& stream = res.value(); + + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + return assetInfo.Name; +} + +uuids::uuid WorkflowAssetList::RetrieveUuidFromFile(const fs::path& file) const +{ + return uuids::uuid::from_string(file.stem().string()); +} + +fs::path WorkflowAssetList::RetrievePathFromAsset(const SavedAsset& asset) const +{ + auto fileName = uuids::to_string(asset.Uuid); + return GetConnectedProject().GetWorkflowPath(fileName); +} + +bool WorkflowAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const +{ + auto path = RetrievePathFromAsset(assetInfo); + auto res = DataArchive::SaveFile(path); + if (!res) return false; + auto& stream = res.value(); + + stream.WriteObject(assetInfo); + // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Workflow + if (auto workflow = static_cast<const Workflow*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + stream.WriteObject(*workflow); + } + + return true; +} + +static std::unique_ptr<Workflow> LoadWorkflowFromFile(const fs::path& path) +{ + auto res = DataArchive::LoadFile(path); + if (!res) return nullptr; + auto& stream = res.value(); + + // TODO this is currently unused + SavedAsset assetInfo; + stream.ReadObject(assetInfo); + + auto workflow = std::make_unique<Workflow>(); + stream.ReadObject(*workflow); + + return workflow; +} + +Workflow* WorkflowAssetList::LoadInstance(const SavedAsset& assetInfo) const +{ + return ::LoadWorkflowFromFile(RetrievePathFromAsset(assetInfo)).release(); +} + +Workflow* WorkflowAssetList::CreateInstance(const SavedAsset& assetInfo) const +{ + return new Workflow(); +} + +bool WorkflowAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const +{ + auto path = RetrievePathFromAsset(assetInfo); + + auto workflow = ::LoadWorkflowFromFile(path); + if (!workflow) return false; + + SaveInstance(assetInfo, workflow.get()); + + return true; +} + +void WorkflowAssetList::DisplayAssetCreator(ListState& state) +{ + auto ValidateNewName = [&]() -> void { + if (mACNewName.empty()) { + mACNewNameError = NameSelectionError::Empty; + return; + } + + if (FindByName(mACNewName)) { + mACNewNameError = NameSelectionError::Duplicated; + return; + } + + mACNewNameError = NameSelectionError::None; + }; + + auto ShowNewNameErrors = [&]() -> void { + switch (mACNewNameError) { + case NameSelectionError::None: break; + case NameSelectionError::Duplicated: + ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR)); + break; + case NameSelectionError::Empty: + ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR)); + break; + } + }; + + auto IsInputValid = [&]() -> bool { + return mACNewNameError == NameSelectionError::None; + }; + + auto ResetState = [&]() -> void { + mACNewName.clear(); + ValidateNewName(); + }; + + if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) { + ValidateNewName(); + } + + ShowNewNameErrors(); + + if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) { + ImGui::CloseCurrentPopup(); + + Create(SavedAsset{ + .Name = mACNewName, + }); + ResetState(); + } + ImGui::SameLine(); + if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) { + ImGui::CloseCurrentPopup(); + } +} + +void WorkflowAssetList::DisplayDetailsTable(ListState& state) const +{ + ImGui::BeginTable("AssetDetailsTable", 1, ImGuiTableFlags_Borders); + + ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME)); + ImGui::TableHeadersRow(); + + for (auto& asset : this->GetAssets()) { + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) { + state.SelectedAsset = &asset; + } + } + + ImGui::EndTable(); +} diff --git a/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp new file mode 100644 index 0000000..ee3da28 --- /dev/null +++ b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp @@ -0,0 +1,143 @@ +#include "Workflow.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/Utils/I18n.hpp> +#include <Cplt/Utils/Macros.hpp> + +#include <memory> + +const char* WorkflowNode::FormatKind(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return I18N_TEXT("Add", L10N_WORKFLOW_ADD); + case KD_NumericSubtraction: return I18N_TEXT("Subtract", L10N_WORKFLOW_SUB); + case KD_NumericMultiplication: return I18N_TEXT("Multiply", L10N_WORKFLOW_MUL); + case KD_NumericDivision: return I18N_TEXT("Divide", L10N_WORKFLOW_DIV); + case KD_NumericExpression: return I18N_TEXT("Evaluate expression", L10N_WORKFLOW_EVAL); + case KD_TextFormatting: return I18N_TEXT("Format text", L10N_WORKFLOW_FMT); + case KD_DocumentTemplateExpansion: return I18N_TEXT("Expand template", L10N_WORKFLOW_INSTANTIATE_TEMPLATE); + case KD_FormInput: return I18N_TEXT("Form input", L10N_WORKFLOW_FORM_INPUT); + case KD_DatabaseRowsInput: return I18N_TEXT("Database input", L10N_WORKFLOW_DB_INPUT); + + case InvalidKind: break; + } + return ""; +} + +const char* WorkflowNode::FormatCategory(WorkflowNode::Category category) +{ + switch (category) { + case CG_Numeric: return I18N_TEXT("Numeric", L10N_WORKFLOW_CATEGORY_NUMERIC); + case CG_Text: return I18N_TEXT("Text", L10N_WORKFLOW_CATEGORY_TEXT); + case CG_Document: return I18N_TEXT("Document", L10N_WORKFLOW_CATEGORY_DOCUMENT); + case CG_UserInput: return I18N_TEXT("User input", L10N_WORKFLOW_CATEGORY_USER_INPUT); + case CG_SystemInput: return I18N_TEXT("System input", L10N_WORKFLOW_CATEGORY_SYS_INPUT); + case CG_Output: return I18N_TEXT("Output", L10N_WORKFLOW_CATEGORY_OUTPUT); + + case InvalidCategory: break; + } + return ""; +} + +const char* WorkflowNode::FormatType(Type type) +{ + switch (type) { + case InputType: return I18N_TEXT("Input", L10N_WORKFLOW_KIND_INPUT); + case TransformType: return I18N_TEXT("Transform", L10N_WORKFLOW_KIND_TRANSFORM); + case OutputType: return I18N_TEXT("Output", L10N_WORKFLOW_KIND_OUTPUT); + } + return ""; +} + +WorkflowNode::Category WorkflowNode::QueryCategory(Kind kind) +{ + switch (kind) { + case KD_NumericAddition: + case KD_NumericSubtraction: + case KD_NumericMultiplication: + case KD_NumericDivision: + case KD_NumericExpression: + return CG_Numeric; + case KD_TextFormatting: + return CG_Text; + case KD_DocumentTemplateExpansion: + return CG_Document; + case KD_FormInput: + case KD_DatabaseRowsInput: + return CG_UserInput; + + case InvalidKind: break; + } + return InvalidCategory; +} + +std::span<const WorkflowNode::Kind> WorkflowNode::QueryCategoryMembers(Category category) +{ + constexpr WorkflowNode::Kind kNumeric[] = { + KD_NumericAddition, + KD_NumericSubtraction, + KD_NumericMultiplication, + KD_NumericDivision, + KD_NumericExpression, + }; + + constexpr WorkflowNode::Kind kText[] = { + KD_TextFormatting, + }; + + constexpr WorkflowNode::Kind kDocument[] = { + KD_DocumentTemplateExpansion, + }; + + constexpr WorkflowNode::Kind kUserInput[] = { + KD_FormInput, + KD_DatabaseRowsInput, + }; + + // TODO remove invalid kinds after we have nodes of these categories + constexpr WorkflowNode::Kind kSystemInput[] = { + InvalidKind, + }; + + constexpr WorkflowNode::Kind kOutput[] = { + InvalidKind, + }; + + switch (category) { + case CG_Numeric: return kNumeric; + case CG_Text: return kText; + case CG_Document: return kDocument; + case CG_UserInput: return kUserInput; + case CG_SystemInput: return kSystemInput; + case CG_Output: return kOutput; + + case InvalidCategory: break; + } + return {}; +} + +std::unique_ptr<WorkflowNode> WorkflowNode::CreateByKind(WorkflowNode::Kind kind) +{ + switch (kind) { + case KD_NumericAddition: return std::make_unique<NumericOperationNode>(NumericOperationNode::Addition); + case KD_NumericSubtraction: return std::make_unique<NumericOperationNode>(NumericOperationNode::Subtraction); + case KD_NumericMultiplication: return std::make_unique<NumericOperationNode>(NumericOperationNode::Multiplication); + case KD_NumericDivision: return std::make_unique<NumericOperationNode>(NumericOperationNode::Division); + case KD_NumericExpression: return std::make_unique<NumericExpressionNode>(); + case KD_TextFormatting: return std::make_unique<TextFormatterNode>(); + case KD_DocumentTemplateExpansion: return std::make_unique<DocumentTemplateExpansionNode>(); + case KD_FormInput: return std::make_unique<FormInputNode>(); + case KD_DatabaseRowsInput: return std::make_unique<DatabaseRowsInputNode>(); + + case InvalidKind: break; + } + return nullptr; +} + +bool WorkflowNode::IsInstance(const WorkflowNode* node) +{ + return true; +} diff --git a/app/source/Cplt/Model/Workflow/fwd.hpp b/app/source/Cplt/Model/Workflow/fwd.hpp new file mode 100644 index 0000000..ce5b6db --- /dev/null +++ b/app/source/Cplt/Model/Workflow/fwd.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <Cplt/Model/Workflow/Nodes/fwd.hpp> +#include <Cplt/Model/Workflow/Values/fwd.hpp> + +// Evaluation.hpp +class WorkflowEvaluationError; +class WorkflowEvaluationContext; + +// SavedWorkflow.hpp +class SavedWorkflowCache; +class SavedWorkflow; + +// Value.hpp +class BaseValue; +class BaseObjectValue; + +// Workflow.hpp +class WorkflowConnection; +class WorkflowNode; +class Workflow; +class WorkflowAssetList; diff --git a/app/source/Cplt/Model/fwd.hpp b/app/source/Cplt/Model/fwd.hpp new file mode 100644 index 0000000..c7e44e6 --- /dev/null +++ b/app/source/Cplt/Model/fwd.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <Cplt/Model/Template/fwd.hpp> +#include <Cplt/Model/Workflow/fwd.hpp> + +// Database.hpp +enum class TableKind; +class SalesTable; +class PurchasesTable; +class DeliveryTable; +class MainDatabase; + +// Assets.hpp +struct SavedAsset; +class Asset; +enum class NameSelectionError; +class AssetList; + +// Filter.hpp +class TableRowsFilter; + +// GlobalStates.hpp +class GlobalStates; + +// Items.hpp +template <class T> +class ItemList; +template <class TSelf> +class ItemBase; +class ProductItem; +class FactoryItem; +class CustomerItem; + +// Project.hpp +class Project; |