diff options
author | rtk0c <[email protected]> | 2021-03-31 20:19:18 -0700 |
---|---|---|
committer | rtk0c <[email protected]> | 2021-03-31 20:19:18 -0700 |
commit | 44f5fa5c8f258e8fc1f7d7e2e45e0485bd6cc490 (patch) | |
tree | 3f09a1cce46d38f5a8c6266150e67af3802d4b95 /core | |
parent | 31950890c939862f79c817053c106bf711c63a64 (diff) |
Complete items tab (UI and serialization)
Diffstat (limited to 'core')
-rw-r--r-- | core/CMakeLists.txt | 1 | ||||
-rw-r--r-- | core/locale/zh_CN.json | 31 | ||||
-rw-r--r-- | core/src/Entrypoint/main.cpp | 58 | ||||
-rw-r--r-- | core/src/Model/GlobalStates.cpp | 17 | ||||
-rw-r--r-- | core/src/Model/GlobalStates.hpp | 5 | ||||
-rw-r--r-- | core/src/Model/Items.cpp | 88 | ||||
-rw-r--r-- | core/src/Model/Items.hpp | 209 | ||||
-rw-r--r-- | core/src/Model/Project.cpp | 66 | ||||
-rw-r--r-- | core/src/Model/Project.hpp | 6 | ||||
-rw-r--r-- | core/src/Model/fwd.hpp | 1 | ||||
-rw-r--r-- | core/src/UI/Localization.hpp | 32 | ||||
-rw-r--r-- | core/src/UI/States.cpp | 4 | ||||
-rw-r--r-- | core/src/UI/States.hpp | 1 | ||||
-rw-r--r-- | core/src/UI/UI.hpp | 9 | ||||
-rw-r--r-- | core/src/UI/UI_Items.cpp | 212 | ||||
-rw-r--r-- | core/src/UI/UI_MainWindow.cpp | 102 | ||||
-rw-r--r-- | core/src/UI/UI_Utils.cpp | 38 | ||||
-rw-r--r-- | core/src/Utils/Enum.hpp | 164 | ||||
-rw-r--r-- | core/src/Utils/I18n.cpp | 520 | ||||
-rw-r--r-- | core/src/Utils/I18n.hpp | 149 | ||||
-rw-r--r-- | core/src/Utils/Sigslot.cpp | 428 | ||||
-rw-r--r-- | core/src/Utils/Sigslot.hpp | 300 | ||||
-rw-r--r-- | core/src/Utils/String.cpp | 340 | ||||
-rw-r--r-- | core/src/Utils/String.hpp | 84 | ||||
-rw-r--r-- | core/src/Utils/fwd.hpp | 40 |
25 files changed, 1440 insertions, 1465 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index f71cdd4..53dd498 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -55,7 +55,6 @@ add_source_group(UTILS_MODULE_SOURCES src/Utils/I18n.cpp src/Utils/Sigslot.cpp src/Utils/StandardDirectories.cpp - src/Utils/String.cpp ) function(add_executable_variant TARGET_NAME) diff --git a/core/locale/zh_CN.json b/core/locale/zh_CN.json index ebd671e..917d2f3 100644 --- a/core/locale/zh_CN.json +++ b/core/locale/zh_CN.json @@ -1,5 +1,6 @@ { "$localized_name": "中文 - 中国", + "Generic.Error": "错误", "Generic.Dialog.Confirm": "确定", "Generic.Dialog.Cancel": "取消", "MainWindow.Tab.Settings": "\uf013 设置", @@ -12,8 +13,8 @@ "Project.New.Name": "项目名称", "Project.New.Path": "项目路径", "Project.New.Path.DialogTitle": "项目路径", - "Project.New.EmptyName": "项目名不能为空", - "Project.New.InvalidPath": "无效路径", + "Project.New.EmptyNameError": "项目名不能为空", + "Project.New.InvalidPathError": "无效路径", "Project.Open": "打开项目...", "Project.Open.DialogTitle": "打开项目", "Project.Recents": "最近使用", @@ -21,16 +22,24 @@ "Project.Recents.NonePresent": "(暂无最近使用的项目)", "Project.Recents.Open.Tooltip": "打开该项目", "Project.Recents.Delete.Tooltip": "将该项目从最近使用列表中删除,项目本身将不受影响。", - "ActiveProject.Close": "\uf410 关闭项目", + "Project.InvalidProjectFormat": "无效的项目文件", + "ActiveProject.Close": "\uf00d 关闭项目", "ActiveProject.OpenInFilesystem": "\uf07b 在文件系统中打开", "ActiveProject.Info.Name": "项目名称:", "ActiveProject.Info.Path": "项目路径:", - "ItemEditor.Add": "\uf067 新建", - "ItemEditor.Add.DialogTitle": "新建物品项", - "ItemEditor.Delete": "\uf1f8 移除", - "Item.Product.CategoryName": "产品", - "Item.Product.Column.Name": "名称", - "Item.Product.Column.Description": "描述", - "Item.Factory.CategoryName": "工厂", - "Item.Customer.CategoryName": "客户", + "Item.Add": "\uf067 新建", + "Item.Add.DialogTitle": "新建物品项", + "Item.Edit": "\uf044 编辑", + "Item.Edit.DialogTitle": "编辑物品项", + "Item.Delete": "\uf1f8 删除", + "Item.Delete.DialogTitle": "删除物品项", + "Item.Delete.DialogMessage": "确定删除该物品项吗?", + "Item.CategoryName.Product": "产品", + "Item.CategoryName.Factory": "工厂", + "Item.CategoryName.Customer": "客户", + "Item.Column.Name": "名称", + "Item.Column.Description": "描述", + "Item.Column.Email": "邮箱", + "Item.EmptyNameError": "产品名不能为空", + "Item.DuplicateNameError": "产品名已被占用", }
\ No newline at end of file diff --git a/core/src/Entrypoint/main.cpp b/core/src/Entrypoint/main.cpp index 5811547..357c333 100644 --- a/core/src/Entrypoint/main.cpp +++ b/core/src/Entrypoint/main.cpp @@ -17,12 +17,14 @@ #include <IconsFontAwesome.h> #include <imgui.h> #include <argparse/argparse.hpp> +#include <filesystem> #include <iostream> #include <memory> #include <stdexcept> #include <string> #include <string_view> +namespace fs = std::filesystem; using namespace std::literals::string_literals; using namespace std::literals::string_view_literals; @@ -114,6 +116,9 @@ static std::unique_ptr<RenderingBackend> CreateBackend(std::string_view option) int main(int argc, char* argv[]) { argparse::ArgumentParser parser; + parser.add_argument("--global-data-directory") + .help("Directory in which global data (such as recently used projects) are saved to. Use 'default' to use the default directory on each platform.") + .default_value("default"s); parser.add_argument("--rendering-backend") .help("Which rendering backend to use. If equals 'default', the preferred API for each platform will be used") .default_value("default"s); @@ -129,31 +134,56 @@ int main(int argc, char* argv[]) { auto backendOption = parser.get<std::string>("--rendering-backend"); auto backend = CreateBackend(backendOption); - ImGui::GetIO().IniFilename = nullptr; - ImGui::GetIO().LogFilename = nullptr; + auto& io = ImGui::GetIO(); + + // Disable saving window positions + io.IniFilename = nullptr; + // Disable log (dump widget tree) file, we don't trigger it but just to be safe + io.LogFilename = nullptr; + + // Light mode because all major OS's default theme is white + // TODO follow system theme ImGui::StyleColorsLight(); - // Includes latin alphabet, although for some reason smaller than if rendered using 18 point NotoSans regular - ImGui::GetIO().Fonts->AddFontFromFileTTF("fonts/NotoSansSC-Regular.otf", 18, nullptr, ImGui::GetIO().Fonts->GetGlyphRangesChineseSimplifiedCommon()); + // Configure default fonts + { + // Includes latin alphabet, although for some reason smaller than if rendered using 18 point NotoSans regular + io.Fonts->AddFontFromFileTTF("fonts/NotoSansSC-Regular.otf", 18, nullptr, io.Fonts->GetGlyphRangesChineseSimplifiedCommon()); + + ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA }; + ImFontConfig config; + config.MergeMode = true; + io.Fonts->AddFontFromFileTTF("fonts/FontAwesome5-Solid.otf", 14, &config, iconRanges); + } - ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA }; - ImFontConfig config; - config.MergeMode = true; - ImGui::GetIO().Fonts->AddFontFromFileTTF("fonts/FontAwesome5-Solid.otf", 14, &config, iconRanges); + // Initialize localization utilities + { + I18n::OnLanguageChange.Connect([]() { LocaleStrings::Instance = std::make_unique<LocaleStrings>(); }); + // Do i18n initialization after linking reload signals, so that when SetLanguage() is called, the locale strings will be initialized (without us writing the code another time outside the slot) + I18n::Init(); + I18n::SetLanguage("zh_CN"); + // All of our usage are cached in XxxTranslation objects, no need to keep key -> entry mappings anymore + I18n::Unload(); + } - I18n::OnReload.Connect([]() { LocaleStrings::Instance = std::make_unique<LocaleStrings>(); }); - // Do i18n initialization after linking reload signals, so that when SetLanguage() is called, the locale strings will be initialized (without us writing the code another time outside the slot) - I18n::Init(); - I18n::SetLanguage("zh_CN"); + auto dataDirOption = parser.get<std::string>("--global-data-directory"); + if (dataDirOption == "default") { + GlobalStates::Init(); + } else { + fs::path path(dataDirOption); + if (fs::exists(path)) { + GlobalStates::Init(std::move(path)); + } else { + GlobalStates::Init(); + } + } - GlobalStates::Init(); UIState::Init(); auto window = backend->GetWindow(); while (!glfwWindowShouldClose(window)) { backend->BeginFrame(); UI::MainWindow(); - ImGui::ShowDemoWindow(); backend->EndFrame(); } diff --git a/core/src/Model/GlobalStates.cpp b/core/src/Model/GlobalStates.cpp index cd076f4..0c4e58e 100644 --- a/core/src/Model/GlobalStates.cpp +++ b/core/src/Model/GlobalStates.cpp @@ -17,8 +17,12 @@ 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 = StandardDirectories::UserData() / "cplt"; + globalDataPath = userDataDir; fs::create_directories(globalDataPath); // Reading recent projects @@ -79,6 +83,17 @@ void GlobalStates::AddRecentProject(const Project& project) { 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()); diff --git a/core/src/Model/GlobalStates.hpp b/core/src/Model/GlobalStates.hpp index e6d823b..8375569 100644 --- a/core/src/Model/GlobalStates.hpp +++ b/core/src/Model/GlobalStates.hpp @@ -10,6 +10,7 @@ class GlobalStates { public: static void Init(); + static void Init(std::filesystem::path userDataDir); static void Shutdown(); static GlobalStates& GetInstance(); @@ -31,6 +32,10 @@ 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); // TODO async autosaving to prevent data loss on crash diff --git a/core/src/Model/Items.cpp b/core/src/Model/Items.cpp index db2d39f..7679eb9 100644 --- a/core/src/Model/Items.cpp +++ b/core/src/Model/Items.cpp @@ -1,37 +1,5 @@ #include "Items.hpp" -#include <limits> -#include <utility> - -ItemBase::ItemBase() - : mId{ std::numeric_limits<size_t>::max() } { -} - -ItemBase::ItemBase(size_t id) - : mId{ id } { -} - -bool ItemBase::IsInvalid() const { - return mId == std::numeric_limits<size_t>::max(); -} - -size_t ItemBase::GetId() const { - return mId; -} - -ProductItem::ProductItem(size_t id, std::string name) - : ItemBase(id) - , mName{ std::move(name) } { -} - -const std::string& ProductItem::GetName() const { - return mName; -} - -void ProductItem::SetName(std::string name) { - mName = std::move(name); -} - const std::string& ProductItem::GetDescription() const { return mDescription; } @@ -40,17 +8,14 @@ void ProductItem::SetDescription(std::string description) { mDescription = std::move(description); } -FactoryItem::FactoryItem(size_t id, std::string name) - : ItemBase(id) - , mName{ std::move(name) } { -} - -const std::string& FactoryItem::GetName() const { - return mName; +Json::Value ProductItem::Serialize() const { + Json::Value elm; + elm["Description"] = mDescription; + return elm; } -void FactoryItem::SetName(std::string name) { - mName = std::move(name); +void ProductItem::Deserialize(const Json::Value& elm) { + mDescription = elm["Description"].asString(); } const std::string& FactoryItem::GetDescription() const { @@ -61,17 +26,24 @@ void FactoryItem::SetDescription(std::string description) { mDescription = std::move(description); } -CustomerItem::CustomerItem(size_t id, std::string name) - : ItemBase(id) - , mName{ std::move(name) } { +const std::string& FactoryItem::GetEmail() const { + return mEmail; } -const std::string& CustomerItem::GetName() const { - return mName; +void FactoryItem::SetEmail(std::string email) { + mEmail = std::move(email); } -void CustomerItem::SetName(std::string name) { - mName = std::move(name); +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 { @@ -81,3 +53,23 @@ const std::string& CustomerItem::GetDescription() const { 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/core/src/Model/Items.hpp b/core/src/Model/Items.hpp index 0c7be41..e20a290 100644 --- a/core/src/Model/Items.hpp +++ b/core/src/Model/Items.hpp @@ -1,54 +1,21 @@ #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> -/// Pointers and references returned by accessors are valid as long as no non-const functions have been called. template <class T> class ItemList { -public: - class Iterator { - private: - typename std::vector<T>::const_iterator mBackingIter; - - public: - Iterator(typename std::vector<T>::const_iterator it) - : mBackingIter{ it } { - } - - Iterator& operator++() { - ++mBackingIter; - return *this; - } - - Iterator& operator++(int) { - auto tmp = *this; - ++mBackingIter; - return tmp; - } - - Iterator& operator--() { - --mBackingIter; - return *this; - } - - Iterator& operator--(int) { - auto tmp = *this; - --mBackingIter; - return tmp; - } - - const T& operator*() const { - return *mBackingIter; - } - - friend bool operator==(const Iterator&, const Iterator&) = default; - }; - private: std::vector<T> mStorage; tsl::array_map<char, size_t> mNameLookup; @@ -61,9 +28,28 @@ public: 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); - return mStorage.emplace_back(id, std::move(name), std::forward<Args>(args)...); + 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 { @@ -79,68 +65,157 @@ public: } } - Iterator begin() const { - return Iterator(mStorage.begin()); + 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; } - Iterator end() const { - return Iterator(mStorage.end()); + 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(); - ItemBase(size_t id); + 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; - size_t GetId() const; + 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 { +class ProductItem : public ItemBase<ProductItem> { private: - std::string mName; std::string mDescription; public: - ProductItem() {} - ProductItem(size_t id, std::string name); + using ItemBase::ItemBase; - const std::string& GetName() const; - void SetName(std::string mName); const std::string& GetDescription() const; void SetDescription(std::string description); + + Json::Value Serialize() const; + void Deserialize(const Json::Value& elm); }; -class FactoryItem : public ItemBase { +class FactoryItem : public ItemBase<FactoryItem> { private: - std::string mName; std::string mDescription; + std::string mEmail; public: - FactoryItem() {} - FactoryItem(size_t id, std::string name); + using ItemBase::ItemBase; - const std::string& GetName() const; - void SetName(std::string name); 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 { +class CustomerItem : public ItemBase<CustomerItem> { private: - std::string mName; std::string mDescription; + std::string mEmail; public: - CustomerItem() {} - CustomerItem(size_t id, std::string name); + using ItemBase::ItemBase; - const std::string& GetName() const; - void SetName(std::string name); 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/core/src/Model/Project.cpp b/core/src/Model/Project.cpp index f070940..cdb88c6 100644 --- a/core/src/Model/Project.cpp +++ b/core/src/Model/Project.cpp @@ -10,40 +10,62 @@ namespace fs = std::filesystem; -Project Project::Load(const fs::path& path) { +template <class T> +void ReadItemList(ItemList<T>& list, const fs::path& filePath) { + std::ifstream ifs(filePath); + if (ifs) { + Json::Value root; + ifs >> root; + + list = ItemList<T>(root); + } +} + +Project Project::Load(const fs::path& projectFilePath) { // TODO better diagnostic const char* kInvalidFormatErr = "Failed to load project: invalid format."; - std::ifstream ifs(path); + std::ifstream ifs(projectFilePath); if (!ifs) { std::string message; message += "Failed to load project file at '"; - message += path.string(); + message += projectFilePath.string(); message += "'."; throw std::runtime_error(message); } Project proj; - proj.mRootPath = path.parent_path(); + proj.mRootPath = projectFilePath.parent_path(); proj.mRootPathString = proj.mRootPath.string(); - Json::Value root; - ifs >> root; + { + 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); - } + 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()) { - proj.mName = name.asString(); - } else { - throw std::runtime_error(kInvalidFormatErr); + if (auto& name = croot["Name"]; name.isString()) { + proj.mName = name.asString(); + } else { + throw std::runtime_error(kInvalidFormatErr); + } } + auto itemsDir = proj.mRootPath / "items"; + ReadItemList(proj.Products, itemsDir / "products.json"); + ReadItemList(proj.Factories, itemsDir / "factories.json"); + ReadItemList(proj.Customers, itemsDir / "customers.json"); + return proj; } +Project Project::LoadDir(const std::filesystem::path& projectPath) { + return Load(projectPath / "cplt_project.json"); +} + Project Project::Create(std::string name, const fs::path& path) { Project proj; proj.mRootPath = path; @@ -76,8 +98,20 @@ Json::Value Project::Serialize() { 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() { - auto root = Serialize(); std::ofstream ofs(mRootPath / "cplt_project.json"); - ofs << root; + ofs << this->Serialize(); + + auto itemsDir = mRootPath / "items"; + fs::create_directories(itemsDir); + + WriteItemList(Products, itemsDir / "products.json"); + WriteItemList(Factories, itemsDir / "factories.json"); + WriteItemList(Customers, itemsDir / "customers.json"); } diff --git a/core/src/Model/Project.hpp b/core/src/Model/Project.hpp index 23eafc1..280eaf3 100644 --- a/core/src/Model/Project.hpp +++ b/core/src/Model/Project.hpp @@ -19,12 +19,14 @@ private: public: /// Load the project from a cplt_project.json file. - static Project Load(const std::filesystem::path& path); + static Project Load(const std::filesystem::path& projectFilePath); + /// Load the project from the directory containing the cplt_project.json file. + static Project LoadDir(const std::filesystem::path& projectPath); /// 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. static Project Create(std::string name, const std::filesystem::path& path); - // Path to a *directory* that contains the project file. + /// Path to a *directory* that contains the project file. const std::filesystem::path& GetPath() const; const std::string& GetPathString() const; diff --git a/core/src/Model/fwd.hpp b/core/src/Model/fwd.hpp index bf9a8cf..146f74a 100644 --- a/core/src/Model/fwd.hpp +++ b/core/src/Model/fwd.hpp @@ -6,6 +6,7 @@ class GlobalStates; // Items.hpp template <class T> class ItemList; +template <class TSelf> class ItemBase; class ProductItem; class FactoryItem; diff --git a/core/src/UI/Localization.hpp b/core/src/UI/Localization.hpp index d5424ea..86b7afc 100644 --- a/core/src/UI/Localization.hpp +++ b/core/src/UI/Localization.hpp @@ -12,6 +12,7 @@ public: static std::unique_ptr<LocaleStrings> Instance; public: + BasicTranslation Error{ "Generic.Error"sv }; BasicTranslation DialogConfirm{ "Generic.Dialog.Confirm"sv }; BasicTranslation DialogCancel{ "Generic.Dialog.Cancel"sv }; @@ -26,8 +27,8 @@ public: BasicTranslation NewProjectNameHint{ "Project.New.Name"sv }; BasicTranslation NewProjectPathHint{ "Project.New.Path"sv }; BasicTranslation NewProjectPathDialogTitle{ "Project.New.Path.DialogTitle"sv }; - BasicTranslation NewProjectEmptyNameError{ "Project.New.EmptyName"sv }; - BasicTranslation NewProjectInvalidPathError{ "Project.New.InvalidPath"sv }; + BasicTranslation NewProjectEmptyNameError{ "Project.New.EmptyNameError"sv }; + BasicTranslation NewProjectInvalidPathError{ "Project.New.InvalidPathError"sv }; BasicTranslation OpenProject{ "Project.Open"sv }; BasicTranslation OpenProjectDialogTitle{ "Project.Open.DialogTitle"sv }; @@ -38,18 +39,29 @@ public: BasicTranslation OpenRecentProjectTooltip{ "Project.Recents.Open.Tooltip"sv }; BasicTranslation DeleteRecentProjectTooltip{ "Project.Recents.Delete.Tooltip"sv }; + BasicTranslation InvalidProjectFormat{ "Project.InvalidProjectFormat"sv }; + BasicTranslation CloseActiveProject{ "ActiveProject.Close"sv }; BasicTranslation OpenActiveProjectInFileSystem{ "ActiveProject.OpenInFilesystem"sv }; BasicTranslation ActiveProjectName{ "ActiveProject.Info.Name"sv }; BasicTranslation ActiveProjectPath{ "ActiveProject.Info.Path"sv }; - BasicTranslation AddItem{ "ItemEditor.Add"sv }; - BasicTranslation AddItemDialogTitle{ "ItemEditor.Add.DialogTitle"sv }; - BasicTranslation DeleteItem{ "ItemEditor.Delete"sv }; + BasicTranslation AddItem{ "Item.Add"sv }; + BasicTranslation AddItemDialogTitle{ "Item.Add.DialogTitle"sv }; + BasicTranslation EditItem{ "Item.Edit"sv }; + BasicTranslation EditItemDialogTitle{ "Item.Edit.DialogTitle"sv }; + BasicTranslation DeleteItem{ "Item.Delete"sv }; + BasicTranslation DeleteItemDialogTitle{ "Item.Delete.DialogTitle"sv }; + BasicTranslation DeleteItemDialogMessage{ "Item.Delete.DialogMessage"sv }; + + BasicTranslation ProductCategoryName{ "Item.CategoryName.Product"sv }; + BasicTranslation FactoryCategoryName{ "Item.CategoryName.Factory"sv }; + BasicTranslation CustomerCategoryName{ "Item.CategoryName.Customer"sv }; + + BasicTranslation ItemNameColumn{ "Item.Column.Name"sv }; + BasicTranslation ItemDescriptionColumn{ "Item.Column.Description"sv }; + BasicTranslation ItemEmailColumn{ "Item.Column.Email"sv }; - BasicTranslation ProductCategoryName{ "Item.Product.CategoryName"sv }; - BasicTranslation ProductNameColumn{ "Item.Product.Column.Name"sv }; - BasicTranslation ProductDescriptionColumn{ "Item.Product.Column.Description"sv }; - BasicTranslation FactoryCategoryName{ "Item.Factory.CategoryName"sv }; - BasicTranslation CustomerCategoryName{ "Item.Customer.CategoryName"sv }; + BasicTranslation EmptyItemNameError{ "Item.EmptyNameError"sv }; + BasicTranslation DuplicateItemNameError{ "Item.DuplicateNameError"sv }; }; diff --git a/core/src/UI/States.cpp b/core/src/UI/States.cpp index dc7c37a..546e1ab 100644 --- a/core/src/UI/States.cpp +++ b/core/src/UI/States.cpp @@ -1,5 +1,6 @@ #include "States.hpp" +#include "Model/GlobalStates.hpp" #include "Model/Project.hpp" #include <memory> @@ -24,6 +25,9 @@ UIState& UIState::GetInstance() { void UIState::SetCurrentProject(std::unique_ptr<Project> project) { CloseCurrentProject(); + if (project) { + GlobalStates::GetInstance().MoveProjectToTop(*project); + } CurrentProject = std::move(project); } diff --git a/core/src/UI/States.hpp b/core/src/UI/States.hpp index de0510c..4cc3b0f 100644 --- a/core/src/UI/States.hpp +++ b/core/src/UI/States.hpp @@ -3,6 +3,7 @@ #include "cplt_fwd.hpp" #include <memory> +#include <imgui.h> /// Minimal state shared by all UI components, such as database, items, export, etc. /// Note that global components (settings) is not supposed to access these. diff --git a/core/src/UI/UI.hpp b/core/src/UI/UI.hpp index 52a2ca9..ab35321 100644 --- a/core/src/UI/UI.hpp +++ b/core/src/UI/UI.hpp @@ -4,14 +4,19 @@ namespace ImGui { -void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond_ cond = ImGuiCond_None); -void SetNextWindowCentered(ImGuiCond_ cond = ImGuiCond_None); +void SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond = ImGuiCond_None); +void SetNextWindowCentered(ImGuiCond cond = ImGuiCond_None); void PushDisabled(); void PopDisabled(); +bool Button(const char* label, bool disabled); +bool Button(const char* label, const ImVec2& sizeArg, bool disabled); + void ErrorIcon(); +void ErrorMessage(const char* fmt, ...); void WarningIcon(); +void WarningMessage(const char* fmt, ...); } // namespace ImGui diff --git a/core/src/UI/UI_Items.cpp b/core/src/UI/UI_Items.cpp index a990a96..970d0df 100644 --- a/core/src/UI/UI_Items.cpp +++ b/core/src/UI/UI_Items.cpp @@ -5,106 +5,202 @@ #include "UI/Localization.hpp" #include "UI/States.hpp" -#include <IconsFontAwesome.h> #include <imgui.h> #include <imgui_stdlib.h> namespace { -/// Specialized for each item type. -template <class T> -void AddToItemListDialog(ItemList<T>& list); -/// Specialized for each item type. -template <class T> -void ItemListEntries(ItemList<T>& list); -template <> -void AddToItemListDialog<ProductItem>(ItemList<ProductItem>& list) { - static std::string productName; - static std::string description; +enum class ActionResult { + Confirmed, + Canceled, + Pending, +}; + +/// \param list Item list that the item is in. +/// \param item A non-null pointer to the currently being edited item. It should not change until this function returns a non-\c ActionResult::Pending value. +template <class T> +ActionResult ItemEditor(ItemList<T>& list, T* item) { + constexpr bool kHasDescription = requires(T t) { t.GetDescription(); }; + constexpr bool kHasEmail = requires(T t) { t.GetEmail(); }; auto ls = LocaleStrings::Instance.get(); auto& uis = UIState::GetInstance(); - ImGui::InputText(ls->ProductNameColumn.Get(), &productName); - ImGui::InputText(ls->ProductDescriptionColumn.Get(), &description); - if (ImGui::Button(ls->DialogConfirm.Get())) { - auto& product = uis.CurrentProject->Products.Insert(std::move(productName)); - product.SetDescription(std::move(description)); + static bool duplicateName = false; + + static std::string name; + static std::string description; + static std::string email; + if (name.empty()) { + name = item->GetName(); + if constexpr (kHasDescription) description = item->GetDescription(); + if constexpr (kHasEmail) email = item->GetEmail(); + } + + auto ClearStates = [&]() { + duplicateName = false; + name = {}; + description = {}; + }; + + if (ImGui::InputText(ls->ItemNameColumn.Get(), &name)) { + duplicateName = name != item->GetName() && list.Find(name) != nullptr; + } + if constexpr (kHasDescription) ImGui::InputText(ls->ItemDescriptionColumn.Get(), &description); + if constexpr (kHasEmail) ImGui::InputText(ls->ItemEmailColumn.Get(), &email); + + if (name.empty()) { + ImGui::ErrorMessage("%s", ls->EmptyItemNameError.Get()); + } + if (duplicateName) { + ImGui::ErrorMessage("%s", ls->DuplicateItemNameError.Get()); + } + + // Return Value + auto rv = ActionResult::Pending; - productName.clear(); - description.clear(); + if (ImGui::Button(ls->DialogConfirm.Get(), name.empty() || duplicateName)) { + if (item->GetName() != name) { + item->SetName(std::move(name)); + } + if constexpr (kHasDescription) + if (item->GetDescription() != description) { + item->SetDescription(std::move(description)); + } + if constexpr (kHasEmail) + if (item->GetEmail() != email) { + item->SetEmail(std::move(email)); + } ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Confirmed; } + ImGui::SameLine(); if (ImGui::Button(ls->DialogCancel.Get())) { ImGui::CloseCurrentPopup(); + ClearStates(); + rv = ActionResult::Canceled; } -} -template <> -void AddToItemListDialog<FactoryItem>(ItemList<FactoryItem>& list) { - // TODO + return rv; } -template <> -void AddToItemListDialog<CustomerItem>(ItemList<CustomerItem>& list) { - // TODO -} +template <class T> +void ItemListEntries(ItemList<T>& list, int& selectedIdx) { + constexpr bool kHasDescription = requires(T t) { t.GetDescription(); }; + constexpr bool kHasEmail = requires(T t) { t.GetEmail(); }; + constexpr int kColumns = 1 /* Name column */ + kHasDescription + kHasEmail; -template <> -void ItemListEntries<ProductItem>(ItemList<ProductItem>& list) { auto ls = LocaleStrings::Instance.get(); - if (ImGui::BeginTable("ItemListEntries", 2)) { + auto& uis = UIState::GetInstance(); - ImGui::TableSetupColumn(ls->ProductNameColumn.Get()); - ImGui::TableSetupColumn(ls->ProductDescriptionColumn.Get()); + if (ImGui::BeginTable("", kColumns, ImGuiTableFlags_Borders)) { + + ImGui::TableSetupColumn(ls->ItemNameColumn.Get()); + if constexpr (kHasDescription) ImGui::TableSetupColumn(ls->ItemDescriptionColumn.Get()); + if constexpr (kHasEmail) ImGui::TableSetupColumn(ls->ItemEmailColumn.Get()); ImGui::TableHeadersRow(); + size_t idx = 0; for (auto& entry : list) { + if (entry.IsInvalid()) { + continue; + } + ImGui::TableNextRow(); + // Field: name ImGui::TableNextColumn(); - ImGui::Text("%s", entry.GetName().c_str()); - ImGui::TableNextColumn(); - ImGui::Text("%.8s", entry.GetDescription().c_str()); - if (ImGui::Button(ICON_FA_EDIT)) { - // TODO + if (ImGui::Selectable(entry.GetName().c_str(), selectedIdx == idx, ImGuiSelectableFlags_SpanAllColumns)) { + selectedIdx = idx; + } + + // Field: description + if constexpr (kHasDescription) { + ImGui::TableNextColumn(); + ImGui::Text("%s", entry.GetDescription().c_str()); + } + + // Field: email + if constexpr (kHasEmail) { + ImGui::TableNextColumn(); + ImGui::Text("%s", entry.GetEmail().c_str()); } + + idx++; } ImGui::EndTable(); } } -template <> -void ItemListEntries<FactoryItem>(ItemList<FactoryItem>& list) { - // TODO -} - -template <> -void ItemListEntries<CustomerItem>(ItemList<CustomerItem>& list) { - // TODO -} - template <class T> void ItemListEditor(ItemList<T>& list) { auto ls = LocaleStrings::Instance.get(); + bool opened = true; + static int selectedIdx = -1; + static T* editingItem = nullptr; + if (ImGui::Button(ls->AddItem.Get())) { ImGui::SetNextWindowCentered(); ImGui::OpenPopup(ls->AddItemDialogTitle.Get()); + + editingItem = &list.Insert(""); + } + if (ImGui::BeginPopupModal(ls->AddItemDialogTitle.Get(), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + switch (ItemEditor(list, editingItem)) { + case ActionResult::Confirmed: + editingItem = nullptr; + break; + case ActionResult::Canceled: + list.Remove(editingItem->GetId()); + editingItem = nullptr; + break; + default: + break; + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ls->EditItem.Get(), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(ls->EditItemDialogTitle.Get()); + + editingItem = list.Find(selectedIdx); } - if (ImGui::BeginPopupModal(ls->AddItemDialogTitle.Get())) { - AddToItemListDialog<T>(list); + if (ImGui::BeginPopupModal(ls->EditItemDialogTitle.Get(), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ItemEditor(list, editingItem) != ActionResult::Pending) { + editingItem = nullptr; + } ImGui::EndPopup(); } ImGui::SameLine(); - if (ImGui::Button(ls->DeleteItem.Get())) { - // TODO + if (ImGui::Button(ls->DeleteItem.Get(), selectedIdx == -1)) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(ls->DeleteItemDialogTitle.Get()); + + list.Remove(selectedIdx); + } + if (ImGui::BeginPopupModal(ls->DeleteItemDialogTitle.Get(), &opened, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("%s", ls->DeleteItemDialogMessage.Get()); + + if (ImGui::Button(ls->DialogConfirm.Get())) { + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button(ls->DialogCancel.Get())) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); } - ItemListEntries<T>(list); + ItemListEntries<T>(list, selectedIdx); } } // namespace @@ -112,13 +208,27 @@ void UI::ItemsTab() { auto ls = LocaleStrings::Instance.get(); auto& uis = UIState::GetInstance(); + constexpr float kAmount = 16.0f; + int id = 0; if (ImGui::CollapsingHeader(ls->ProductCategoryName.Get())) { + ImGui::PushID(id++); + ImGui::Indent(kAmount); ItemListEditor(uis.CurrentProject->Products); + ImGui::Unindent(kAmount); + ImGui::PopID(); } if (ImGui::CollapsingHeader(ls->FactoryCategoryName.Get())) { + ImGui::PushID(id++); + ImGui::Indent(kAmount); ItemListEditor(uis.CurrentProject->Factories); + ImGui::Unindent(kAmount); + ImGui::PopID(); } if (ImGui::CollapsingHeader(ls->CustomerCategoryName.Get())) { + ImGui::PushID(id++); + ImGui::Indent(kAmount); ItemListEditor(uis.CurrentProject->Customers); + ImGui::Unindent(kAmount); + ImGui::PopID(); } } diff --git a/core/src/UI/UI_MainWindow.cpp b/core/src/UI/UI_MainWindow.cpp index 15c28ff..661c535 100644 --- a/core/src/UI/UI_MainWindow.cpp +++ b/core/src/UI/UI_MainWindow.cpp @@ -15,10 +15,16 @@ namespace fs = std::filesystem; namespace { -void LoadProjectAt(const std::filesystem::path& path) { +bool LoadProjectAt(const std::filesystem::path& path) { auto& uis = UIState::GetInstance(); - auto project = Project::Load(path); - uis.SetCurrentProject(std::make_unique<Project>(std::move(project))); + try { + auto project = Project::Load(path); + uis.SetCurrentProject(std::make_unique<Project>(std::move(project))); + + return true; + } catch (const std::exception& e) { + return false; + } } void ProjectTab_Normal() { @@ -44,6 +50,8 @@ void ProjectTab_NoProject() { auto& gs = GlobalStates::GetInstance(); auto& uis = UIState::GetInstance(); + bool openedDummy = true; + bool openErrorDialog = false; static std::string projectName; static std::string dirName; static fs::path dirPath; @@ -61,13 +69,11 @@ void ProjectTab_NoProject() { if (ImGui::Button(ls->NewProject.Get())) { ImGui::SetNextWindowCentered(); - ImGui::SetNextWindowSizeRelScreen(0.5f, 0.5f); ImGui::OpenPopup(ls->NewProjectDialogTitle.Get()); } // Make it so that the modal dialog has a close button - bool newProjectDialogDummyTrue = true; - if (ImGui::BeginPopupModal(ls->NewProjectDialogTitle.Get(), &newProjectDialogDummyTrue)) { + if (ImGui::BeginPopupModal(ls->NewProjectDialogTitle.Get(), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::InputTextWithHint("##ProjectName", ls->NewProjectNameHint.Get(), &projectName); if (ImGui::InputTextWithHint("##ProjectPath", ls->NewProjectPathHint.Get(), &dirName)) { @@ -83,25 +89,15 @@ void ProjectTab_NoProject() { } if (projectName.empty()) { - ImGui::ErrorIcon(); - - ImGui::SameLine(); - ImGui::Text("%s", ls->NewProjectEmptyNameError.Get()); + ImGui::ErrorMessage("%s", ls->NewProjectEmptyNameError.Get()); } - if (!dirNameIsValid) { - ImGui::ErrorIcon(); - - ImGui::SameLine(); - ImGui::Text("%s", ls->NewProjectInvalidPathError.Get()); + ImGui::ErrorMessage("%s", ls->NewProjectInvalidPathError.Get()); } ImGui::Spacing(); - bool formValid = dirNameIsValid && !projectName.empty(); - - if (!formValid) ImGui::PushDisabled(); - if (ImGui::Button(ls->DialogConfirm.Get())) { + if (ImGui::Button(ls->DialogConfirm.Get(), !dirNameIsValid || projectName.empty())) { ImGui::CloseCurrentPopup(); auto project = Project::Create(std::move(projectName), dirPath); @@ -113,7 +109,6 @@ void ProjectTab_NoProject() { dirPath = fs::path{}; dirNameIsValid = false; } - if (!formValid) ImGui::PopDisabled(); ImGui::SameLine(); if (ImGui::Button(ls->DialogCancel.Get())) { @@ -123,45 +118,72 @@ void ProjectTab_NoProject() { ImGui::EndPopup(); } + ImGui::SameLine(); if (ImGui::Button(ls->OpenProject.Get())) { auto selection = pfd::open_file(ls->OpenProjectDialogTitle.Get()).result(); if (!selection.empty()) { fs::path path(selection[0]); - LoadProjectAt(path); + openErrorDialog = !LoadProjectAt(path); } } + // TODO cleanup UI + // Recent projects + ImGui::Separator(); ImGui::Text("%s", ls->RecentProjects.Get()); + ImGui::SameLine(); if (ImGui::Button(ls->ClearRecentProjects.Get())) { gs.ClearRecentProjects(); } - auto& recentProjects = gs.GetRecentProjects(); - if (recentProjects.empty()) { + auto& rp = gs.GetRecentProjects(); + // End of vector is the most recently used, so that appending has less overhead + size_t toRemoveIdx = rp.size(); + + if (rp.empty()) { ImGui::Text("%s", ls->NoRecentProjectsMessage.Get()); - } - for (auto it = recentProjects.begin(); it != recentProjects.end(); ++it) { - auto& [path, recent] = *it; - ImGui::Text("%s", recent.c_str()); + } else { + for (auto it = rp.rbegin(); it != rp.rend(); ++it) { + auto& [path, recent] = *it; + ImGui::Text("%s", recent.c_str()); - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_EDIT)) { - LoadProjectAt(path); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", ls->OpenRecentProjectTooltip.Get()); - } + size_t idx = std::distance(it, rp.rend()) - 1; + ImGui::PushID(idx); - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_TRASH)) { - gs.RemoveRecentProject(std::distance(recentProjects.begin(), it)); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", ls->DeleteRecentProjectTooltip.Get()); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN)) { + openErrorDialog = !LoadProjectAt(path / "cplt_project.json"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", ls->OpenRecentProjectTooltip.Get()); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH)) { + toRemoveIdx = idx; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", ls->DeleteRecentProjectTooltip.Get()); + } + + ImGui::PopID(); } } + + if (toRemoveIdx != rp.size()) { + gs.RemoveRecentProject(toRemoveIdx); + } + + if (openErrorDialog) { + ImGui::SetNextWindowCentered(); + ImGui::OpenPopup(ls->Error.Get()); + } + if (ImGui::BeginPopupModal(ls->Error.Get(), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::ErrorMessage("%s", ls->InvalidProjectFormat.Get()); + ImGui::EndPopup(); + } } } // namespace diff --git a/core/src/UI/UI_Utils.cpp b/core/src/UI/UI_Utils.cpp index 615caae..06fd55e 100644 --- a/core/src/UI/UI_Utils.cpp +++ b/core/src/UI/UI_Utils.cpp @@ -4,18 +4,18 @@ #include <imgui.h> #include <imgui_internal.h> -void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond_ cond) { +void ImGui::SetNextWindowSizeRelScreen(float xPercent, float yPercent, ImGuiCond cond) { auto vs = ImGui::GetMainViewport()->Size; ImGui::SetNextWindowSize({ vs.x * xPercent, vs.y * yPercent }, cond); } -void ImGui::SetNextWindowCentered(ImGuiCond_ cond) { +void ImGui::SetNextWindowCentered(ImGuiCond cond) { auto vs = ImGui::GetMainViewport()->Size; ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, cond, { 0.5f, 0.5f }); } void ImGui::PushDisabled() { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, false); + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha); } @@ -24,14 +24,46 @@ void ImGui::PopDisabled() { ImGui::PopStyleVar(); } +bool ImGui::Button(const char* label, bool disabled) { + return Button(label, ImVec2{}, disabled); +} + +bool ImGui::Button(const char* label, const ImVec2& sizeArg, bool disabled) { + if (disabled) PushDisabled(); + bool res = ImGui::Button(label, sizeArg); + if (disabled) PopDisabled(); + + return res; +} + void ImGui::ErrorIcon() { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 237 / 255.0f, 67 / 255.0f, 55 / 255.0f, 1.0f }); // #ED4337 ImGui::Text(ICON_FA_EXCLAMATION_CIRCLE); ImGui::PopStyleColor(); } +void ImGui::ErrorMessage(const char* fmt, ...) { + ErrorIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + void ImGui::WarningIcon() { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{ 255 / 255.0f, 184 / 255.0f, 24 / 255.0f, 1.0f }); // #FFB818 ImGui::Text(ICON_FA_EXCLAMATION_TRIANGLE); ImGui::PopStyleColor(); } + +void ImGui::WarningMessage(const char* fmt, ...) { + WarningIcon(); + SameLine(); + + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} diff --git a/core/src/Utils/Enum.hpp b/core/src/Utils/Enum.hpp index 5075155..e774b01 100644 --- a/core/src/Utils/Enum.hpp +++ b/core/src/Utils/Enum.hpp @@ -1,82 +1,82 @@ -#pragma once
-
-#include <compare>
-
-template <class TSelf>
-struct BasicEnum {
- using Self = TSelf;
- using ValueType = int;
-
- int value;
-
- BasicEnum()
- : value{ 0 } {}
- BasicEnum(int value)
- : value{ value } {}
-
- /// Comparsion between 2 values of enum. Useful for situations like `a == b` where both a and b are TSelf.
- friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; }
- /// Comparsion between a enum and a raw value. Useful for situations like `a == TSelf::Option` where a is a enum and TSelf::Option is a raw value.
- friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; }
-
- operator int() const { return value; }
- operator bool() const { return value; }
-};
-
-#define ENUM(Name) struct Name : public BasicEnum<Name>
-#define ENUM_MEMBERS() \
- enum Enum : int; \
- operator Enum() const { return (Enum)value; } \
- using BasicEnum<Self>::BasicEnum; \
- enum Enum : int
-
-template <class TSelf>
-struct BasicFlag {
- using Self = TSelf;
- using ValueType = int;
-
- int value;
-
- BasicFlag()
- : value{ 0 } {}
- BasicFlag(int value)
- : value{ value } {}
-
- bool IsSet(TSelf mask) const { return (value & mask.value) == mask.value; }
- bool IsSetExclusive(TSelf mask) const { return value == mask.value; }
- void Set(TSelf mask, bool state) {
- if (state) {
- value = (int)(value | mask.value);
- } else {
- value = (int)(value & ~mask.value);
- }
- }
-
- /// Comparsion between 2 values of flag. Useful for situations like `a == b` where both a and b are TSelf.
- friend bool operator==(const TSelf& a, const TSelf& b) { return a.value == b.value; }
- friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; }
- /// Comparsion between a flag and a raw value. Useful for situations like `a == TSelf::Option` where a is a flag and TSelf::Option is a raw value.
- friend bool operator==(const TSelf& self, int value) { return self.value == value; }
- friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; }
-
- friend TSelf operator&(const TSelf& a, const TSelf& b) { return TSelf(a.value & b.value); }
- friend TSelf operator|(const TSelf& a, const TSelf& b) { return TSelf(a.value | b.value); }
- friend TSelf operator^(const TSelf& a, const TSelf& b) { return TSelf(a.value ^ b.value); }
-
- TSelf operator~() const { return TSelf(~value); }
-
- TSelf& operator&=(int that) {
- value = value & that;
- return *this;
- }
-
- TSelf& operator|=(int that) {
- value = value | that;
- return *this;
- }
-
- TSelf& operator^=(int that) {
- value = value ^ that;
- return *this;
- }
-};
+#pragma once + +#include <compare> + +template <class TSelf> +struct BasicEnum { + using Self = TSelf; + using ValueType = int; + + int value; + + BasicEnum() + : value{ 0 } {} + BasicEnum(int value) + : value{ value } {} + + /// Comparsion between 2 values of enum. Useful for situations like `a == b` where both a and b are TSelf. + friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; } + /// Comparsion between a enum and a raw value. Useful for situations like `a == TSelf::Option` where a is a enum and TSelf::Option is a raw value. + friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; } + + operator int() const { return value; } + operator bool() const { return value; } +}; + +#define ENUM(Name) struct Name : public BasicEnum<Name> +#define ENUM_MEMBERS() \ + enum Enum : int; \ + operator Enum() const { return (Enum)value; } \ + using BasicEnum<Self>::BasicEnum; \ + enum Enum : int + +template <class TSelf> +struct BasicFlag { + using Self = TSelf; + using ValueType = int; + + int value; + + BasicFlag() + : value{ 0 } {} + BasicFlag(int value) + : value{ value } {} + + bool IsSet(TSelf mask) const { return (value & mask.value) == mask.value; } + bool IsSetExclusive(TSelf mask) const { return value == mask.value; } + void Set(TSelf mask, bool state) { + if (state) { + value = (int)(value | mask.value); + } else { + value = (int)(value & ~mask.value); + } + } + + /// Comparsion between 2 values of flag. Useful for situations like `a == b` where both a and b are TSelf. + friend bool operator==(const TSelf& a, const TSelf& b) { return a.value == b.value; } + friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; } + /// Comparsion between a flag and a raw value. Useful for situations like `a == TSelf::Option` where a is a flag and TSelf::Option is a raw value. + friend bool operator==(const TSelf& self, int value) { return self.value == value; } + friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; } + + friend TSelf operator&(const TSelf& a, const TSelf& b) { return TSelf(a.value & b.value); } + friend TSelf operator|(const TSelf& a, const TSelf& b) { return TSelf(a.value | b.value); } + friend TSelf operator^(const TSelf& a, const TSelf& b) { return TSelf(a.value ^ b.value); } + + TSelf operator~() const { return TSelf(~value); } + + TSelf& operator&=(int that) { + value = value & that; + return *this; + } + + TSelf& operator|=(int that) { + value = value | that; + return *this; + } + + TSelf& operator^=(int that) { + value = value ^ that; + return *this; + } +}; diff --git a/core/src/Utils/I18n.cpp b/core/src/Utils/I18n.cpp index 50edf67..edc5469 100644 --- a/core/src/Utils/I18n.cpp +++ b/core/src/Utils/I18n.cpp @@ -1,240 +1,280 @@ -#include "I18n.hpp"
-
-#include "Utils/String.hpp"
-
-#include <json/reader.h>
-#include <json/value.h>
-#include <tsl/array_map.h>
-#include <filesystem>
-#include <fstream>
-#include <stdexcept>
-#include <string_view>
-#include <utility>
-
-namespace fs = std::filesystem;
-namespace {
-
-struct LanguageInfo {
- std::string codeName;
- std::string localeName;
- fs::path file;
-};
-
-class I18nState {
-public:
- static I18nState& Get() {
- static I18nState instance;
- return instance;
- }
-
-public:
- tsl::array_map<char, LanguageInfo> LocaleInfos;
- tsl::array_map<char, std::string> CurrentEntries;
- LanguageInfo* CurrentLanguage = nullptr;
-};
-
-std::string FindLocalizedName(const fs::path& localeFile) {
- std::ifstream ifs{ localeFile };
- if (!ifs) {
- throw std::runtime_error("Failed to open locale file.");
- }
-
- Json::Value root;
- ifs >> root;
- if (auto& name = root["$localized_name"]; name.isString()) {
- return std::string(name.asCString());
- } else {
- throw std::runtime_error("Failed to find $localized_name in language file.");
- }
-}
-
-} // namespace
-
-void I18n::Init() {
- auto& state = I18nState::Get();
-
- auto dir = fs::current_path() / "locale";
- if (!fs::exists(dir)) {
- throw std::runtime_error("Failed to find locale directory.");
- }
-
- for (auto& elm : fs::directory_iterator{ dir }) {
- if (!elm.is_regular_file()) continue;
-
- auto& path = elm.path();
- auto codeName = path.stem().string();
-
- state.LocaleInfos.emplace(
- codeName,
- LanguageInfo{
- .codeName = codeName,
- .localeName = FindLocalizedName(path),
- .file = path,
- });
- }
-}
-
-void I18n::Shutdown() {
- // Nothing to do yet
-}
-
-Signal<> I18n::OnReload{};
-
-void I18n::ReloadLocales() {
- auto& state = I18nState::Get();
- OnReload();
-}
-
-std::string_view I18n::GetLanguage() {
- auto& state = I18nState::Get();
- return state.CurrentLanguage->localeName;
-}
-
-bool I18n::SetLanguage(std::string_view lang) {
- auto& state = I18nState::Get();
- if (state.CurrentLanguage &&
- state.CurrentLanguage->codeName == lang)
- {
- return false;
- }
-
- if (auto iter = state.LocaleInfos.find(lang); iter != state.LocaleInfos.end()) {
- state.CurrentLanguage = &iter.value();
- state.CurrentEntries.clear();
-
- auto& file = state.CurrentLanguage->file;
- std::ifstream ifs{ file };
- Json::Value root;
- ifs >> root;
-
- for (auto name : root.getMemberNames()) {
- if (name == "$localized_name") {
- continue;
- }
-
- auto& value = root[name];
- if (value.isString()) {
- state.CurrentEntries.insert(name, value.asCString());
- }
- }
- }
- ReloadLocales();
- return true;
-}
-
-std::optional<std::string_view> I18n::Lookup(std::string_view key) {
- auto& state = I18nState::Get();
- auto iter = state.CurrentEntries.find(key);
- if (iter != state.CurrentEntries.end()) {
- return iter.value();
- } else {
- return std::nullopt;
- }
-}
-
-std::string_view I18n::LookupUnwrap(std::string_view key) {
- auto o = Lookup(key);
- if (!o) {
- std::string msg;
- msg.append("Unable to find locale for '");
- msg.append(key);
- msg.append("'.");
- throw std::runtime_error(std::move(msg));
- };
- return o.value();
-}
-
-BasicTranslation::BasicTranslation(std::string_view key)
- // Assuming the string is null terminated, which it is here (because we store interally using std::string)
- // TODO properly use std::string_view when imgui supports it
- : mContent{ I18n::LookupUnwrap(key).data() } {
-}
-
-const char* BasicTranslation::Get() const {
- return mContent;
-}
-
-FormattedTranslation::FormattedTranslation(std::string_view key) {
- auto src = I18n::LookupUnwrap(key);
-
- mMinimumResultLen = 0;
-
- bool escape = false;
- bool matchingCloseBrace = false;
- std::string buf;
- for (char c : src) {
- switch (c) {
- case '\\': {
- // Disallow double (or more) escaping
- if (escape) throw std::runtime_error("Cannot escape '\\'.");
-
- escape = true;
- continue;
- }
-
- case '{': {
- // Escaping an opeing brace cause the whole "argument" (if any) gets parsed as a part of the previous literal
- if (escape) {
- buf += '{';
- break;
- }
-
- // Generate literal
- mMinimumResultLen += buf.size();
- mParsedElements.push_back(Element{ std::move(buf) }); // Should also clear buf
-
- matchingCloseBrace = true;
- } break;
- case '}': {
- if (escape) throw std::runtime_error("Cannot escape '}', put \\ before the '{' if intended to escape braces.");
-
- // If there is no pairing '{', simply treat this as a normal character
- // (escaping for closing braces)
- if (!matchingCloseBrace) {
- buf += '}';
- break;
- }
-
- // Generate argument
- if (buf.empty()) {
- // No index given, default to use current argument's index
- auto currArgIdx = (int)mNumArguments;
- mParsedElements.push_back(Element{ currArgIdx });
- } else {
- // Use provided index
- int argIdx = std::stoi(buf);
- mParsedElements.push_back(Element{ argIdx });
- buf.clear();
- }
- } break;
-
- default: {
- if (escape) throw std::runtime_error("Cannot escape normal character '" + std::to_string(c) + "'.");
-
- buf += c;
- } break;
- }
-
- escape = false;
- }
-}
-
-std::string FormattedTranslation::Format(std::span<Argument> args) {
- if (args.size() != mNumArguments) {
- throw std::runtime_error("Invalid number of arguments for FormattedTranslation::Format, expected " + std::to_string(mNumArguments) + " but found " + std::to_string(args.size()) + ".");
- }
-
- std::string result;
- result.reserve(mMinimumResultLen);
-
- for (auto& elm : mParsedElements) {
- if (auto literal = std::get_if<std::string>(&elm)) {
- result.append(*literal);
- }
- if (auto idx = std::get_if<int>(&elm)) {
- result.append(args[*idx]);
- }
- }
-
- return result;
-}
+#include "I18n.hpp" + +#include <json/reader.h> +#include <json/value.h> +#include <tsl/array_map.h> +#include <filesystem> +#include <fstream> +#include <stdexcept> +#include <string_view> +#include <utility> + +namespace fs = std::filesystem; +using namespace std::literals::string_view_literals; + +namespace { + +struct LanguageInfo { + std::string CodeName; + std::string LocaleName; + fs::path File; +}; + +class I18nState { +public: + static I18nState& Get() { + static I18nState instance; + return instance; + } + +public: + tsl::array_map<char, LanguageInfo> LocaleInfos; + tsl::array_map<char, std::string> CurrentEntries; + LanguageInfo* CurrentLanguage = nullptr; + bool Unloaded = false; + + void Unload() { + Unloaded = true; + CurrentEntries = {}; + } + + void EnsureLoaded() { + if (Unloaded) { + Unloaded = false; + Reload(); + } + } + + void Reload() { + if (!CurrentLanguage) return; + + std::ifstream ifs(CurrentLanguage->File); + Json::Value root; + ifs >> root; + + for (auto name : root.getMemberNames()) { + if (name == "$localized_name") { + continue; + } + + auto& value = root[name]; + if (value.isString()) { + CurrentEntries.insert(name, value.asCString()); + } + } + } +}; + +std::string FindLocalizedName(const fs::path& localeFile) { + std::ifstream ifs(localeFile); + if (!ifs) { + throw std::runtime_error("Failed to open locale file."); + } + + Json::Value root; + ifs >> root; + if (auto& name = root["$localized_name"]; name.isString()) { + return std::string(name.asCString()); + } else { + throw std::runtime_error("Failed to find $localized_name in language file."); + } +} + +} // namespace + +void I18n::Init() { + auto& state = I18nState::Get(); + + auto dir = fs::current_path() / "locale"; + if (!fs::exists(dir)) { + throw std::runtime_error("Failed to find locale directory."); + } + + for (auto& elm : fs::directory_iterator{ dir }) { + if (!elm.is_regular_file()) continue; + + auto& path = elm.path(); + auto codeName = path.stem().string(); + + state.LocaleInfos.emplace( + codeName, + LanguageInfo{ + .CodeName = codeName, + .LocaleName = FindLocalizedName(path), + .File = path, + }); + } +} + +void I18n::Shutdown() { + auto& state = I18nState::Get(); + state.LocaleInfos.clear(); + state.CurrentEntries.clear(); + state.CurrentLanguage = nullptr; + state.Unloaded = false; +} + +void I18n::Unload() { + auto& state = I18nState::Get(); + state.Unload(); + OnUnload(); +} + +std::string_view I18n::GetLanguage() { + auto& state = I18nState::Get(); + return state.CurrentLanguage->CodeName; +} + +bool I18n::SetLanguage(std::string_view lang) { + auto& state = I18nState::Get(); + if (state.CurrentLanguage && + state.CurrentLanguage->CodeName == lang) + { + return false; + } + + if (auto iter = state.LocaleInfos.find(lang); iter != state.LocaleInfos.end()) { + state.CurrentLanguage = &iter.value(); + state.Reload(); + } + + OnLanguageChange(); + return true; +} + +std::optional<std::string_view> I18n::Lookup(std::string_view key) { + auto& state = I18nState::Get(); + state.EnsureLoaded(); + + auto iter = state.CurrentEntries.find(key); + if (iter != state.CurrentEntries.end()) { + return iter.value(); + } else { + return std::nullopt; + } +} + +std::string_view I18n::LookupUnwrap(std::string_view key) { + auto o = Lookup(key); + if (!o) { + std::string msg; + msg.append("Unable to find locale for '"); + msg.append(key); + msg.append("'."); + throw std::runtime_error(std::move(msg)); + }; + return o.value(); +} + +std::string_view I18n::LookupLanguage(std::string_view lang) { + auto& state = I18nState::Get(); + auto iter = state.LocaleInfos.find(lang); + if (iter != state.LocaleInfos.end()) { + return iter.value().LocaleName; + } else { + return ""sv; + } +} + +BasicTranslation::BasicTranslation(std::string_view key) + : mContent{ I18n::LookupUnwrap(key) } { +} + +const std::string& BasicTranslation::GetString() const { + return mContent; +} + +const char* BasicTranslation::Get() const { + return mContent.c_str(); +} + +FormattedTranslation::FormattedTranslation(std::string_view key) { + auto src = I18n::LookupUnwrap(key); + + mMinimumResultLen = 0; + + bool escape = false; + bool matchingCloseBrace = false; + std::string buf; + for (char c : src) { + switch (c) { + case '\\': { + // Disallow double (or more) escaping + if (escape) { + buf += '\\'; + escape = false; + break; + } + + escape = true; + } break; + + case '{': { + // Escaping an opening brace cause the whole "argument" (if any) gets parsed as a part of the previous literal + if (escape) { + buf += '{'; + escape = false; + break; + } + + // Generate literal + mMinimumResultLen += buf.size(); + mParsedElements.push_back(Element{ std::move(buf) }); // Should also clear buf + + matchingCloseBrace = true; + } break; + case '}': { + if (escape) { + throw std::runtime_error("Cannot escape '}', put \\ before the '{' if intended to escape braces."); + } + + // If there is no pairing '{', simply treat this as a normal character + // (escaping for closing braces) + if (!matchingCloseBrace) { + buf += '}'; + break; + } + + // Generate argument + if (buf.empty()) { + // No index given, default to use current argument's index + auto currArgIdx = (int)mNumArguments; + mParsedElements.push_back(Element{ currArgIdx }); + } else { + // Use provided index + int argIdx = std::stoi(buf); + mParsedElements.push_back(Element{ argIdx }); + buf.clear(); + } + } break; + + default: { + if (escape) { + throw std::runtime_error("Cannot escape normal character '" + std::to_string(c) + "'."); + } + + buf += c; + } break; + } + } +} + +std::string FormattedTranslation::Format(std::span<Argument> args) { + if (args.size() != mNumArguments) { + throw std::runtime_error("Invalid number of arguments for FormattedTranslation::Format, expected " + std::to_string(mNumArguments) + " but found " + std::to_string(args.size()) + "."); + } + + std::string result; + result.reserve(mMinimumResultLen); + + for (auto& elm : mParsedElements) { + if (auto literal = std::get_if<std::string>(&elm)) { + result.append(*literal); + } + if (auto idx = std::get_if<int>(&elm)) { + result.append(args[*idx]); + } + } + + return result; +} diff --git a/core/src/Utils/I18n.hpp b/core/src/Utils/I18n.hpp index de32cf8..a4dd225 100644 --- a/core/src/Utils/I18n.hpp +++ b/core/src/Utils/I18n.hpp @@ -1,69 +1,80 @@ -#pragma once
-
-#include "Utils/Sigslot.hpp"
-#include "Utils/fwd.hpp"
-
-#include <cstddef>
-#include <optional>
-#include <span>
-#include <string>
-#include <string_view>
-#include <variant>
-#include <vector>
-
-class I18n {
-public:
- static void Init();
- static void Shutdown();
-
- static Signal<> OnReload;
- static void ReloadLocales();
-
- static std::string_view GetLanguage();
- static bool SetLanguage(std::string_view lang);
-
- static std::optional<std::string_view> Lookup(std::string_view key);
- static std::string_view LookupUnwrap(std::string_view key);
- static std::string_view LookupLanguage(std::string_view lang);
-};
-
-struct StringArgument {
- std::string Value;
-};
-
-struct IntArgument {
- int Value;
-};
-
-struct FloatArgument {
- double Value;
-};
-
-class BasicTranslation {
-private:
- const char* mContent;
-
-public:
- BasicTranslation(std::string_view key);
- const char* Get() const;
-};
-
-class FormattedTranslation {
-public:
- using Element = std::variant<std::string, int>;
- using Argument = std::string;
-
-private:
- std::vector<Element> mParsedElements;
- size_t mNumArguments;
- size_t mMinimumResultLen;
-
-public:
- FormattedTranslation(std::string_view key);
- std::string Format(std::span<Argument> args);
-};
-
-class NumericTranslation {
-public:
- // TODO
-};
+#pragma once + +#include "Utils/Sigslot.hpp" +#include "Utils/fwd.hpp" + +#include <cstddef> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <variant> +#include <vector> + +class I18n { +public: + static inline Signal<> OnLanguageChange{}; + static inline Signal<> OnUnload{}; + + static void Init(); + static void Shutdown(); + + /// Discard in-memory mapping from key to locale entries. + /// When any of the entry accessors are invoked, unloaded entries will be reloaded. + static void Unload(); + + static std::string_view GetLanguage(); + static bool SetLanguage(std::string_view lang); + + /* Entry accessors */ + /// Find the localized entry with the given key, return \c std::nullopt if does not exist. Reloads locale entries if they are currently unloaded. + static std::optional<std::string_view> Lookup(std::string_view key); + /// Find the localized entry with the given key, throw an exception if does not exist. EnsureLoaded locale entries if they are currently unloaded. + static std::string_view LookupUnwrap(std::string_view key); + + /// Query the localized name of a locale, e.g. "en_US" -> "English - United States". + /// If the queried locale does not exist, an empty string will be returned (existing locales can never have an empty localized name). + static std::string_view LookupLanguage(std::string_view lang); +}; + +struct StringArgument { + std::string Value; +}; + +struct IntArgument { + int Value; +}; + +struct FloatArgument { + double Value; +}; + +class BasicTranslation { +private: + std::string mContent; + +public: + BasicTranslation(std::string_view key); + const std::string& GetString() const; + const char* Get() const; +}; + +class FormattedTranslation { +public: + using Element = std::variant<std::string, int>; + using Argument = std::string; + +private: + std::vector<Element> mParsedElements; + size_t mNumArguments; + size_t mMinimumResultLen; + +public: + FormattedTranslation(std::string_view key); + std::string Format(std::span<Argument> args); +}; + +class NumericTranslation { +public: + // TODO +}; diff --git a/core/src/Utils/Sigslot.cpp b/core/src/Utils/Sigslot.cpp index a36eb2f..14deece 100644 --- a/core/src/Utils/Sigslot.cpp +++ b/core/src/Utils/Sigslot.cpp @@ -1,214 +1,214 @@ -#include "Sigslot.hpp"
-
-#include <doctest/doctest.h>
-
-bool SignalStub::Connection::IsOccupied() const {
- return id != InvalidId;
-}
-
-SignalStub::SignalStub(IWrapper& wrapper)
- : mWrapper{ &wrapper } {
-}
-
-SignalStub::~SignalStub() {
- RemoveAllConnections();
-}
-
-std::span<const SignalStub::Connection> SignalStub::GetConnections() const {
- return mConnections;
-}
-
-SignalStub::Connection& SignalStub::InsertConnection(SlotGuard* guard) {
- Connection* result;
- int size = static_cast<int>(mConnections.size());
- for (int i = 0; i < size; ++i) {
- auto& conn = mConnections[i];
- if (!conn.IsOccupied()) {
- result = &conn;
- result->id = i;
- goto setup;
- }
- }
-
- mConnections.push_back(Connection{});
- result = &mConnections.back();
- result->id = size;
-
-setup:
- if (guard) {
- result->guard = guard;
- result->slotId = guard->InsertConnection(*this, result->id);
- }
- return *result;
-}
-
-void SignalStub::RemoveConnection(int id) {
- if (id >= 0 && id < mConnections.size()) {
- auto& conn = mConnections[id];
- if (conn.IsOccupied()) {
- mWrapper->RemoveFunction(conn.id);
- if (conn.guard) {
- conn.guard->RemoveConnection(conn.slotId);
- }
-
- conn.guard = nullptr;
- conn.slotId = SignalStub::InvalidId;
- conn.id = SignalStub::InvalidId;
- }
- }
-}
-
-void SignalStub::RemoveConnectionFor(SlotGuard& guard) {
- guard.RemoveConnectionFor(*this);
-}
-
-void SignalStub::RemoveAllConnections() {
- for (size_t i = 0; i < mConnections.size(); ++i) {
- RemoveConnection(i);
- }
-}
-
-SlotGuard::SlotGuard() {
-}
-
-SlotGuard::~SlotGuard() {
- DisconnectAll();
-}
-
-void SlotGuard::DisconnectAll() {
- for (auto& conn : mConnections) {
- if (conn.stub) {
- // Also calls SlotGuard::removeConnection, our copy of the data will be cleared in it
- conn.stub->RemoveConnection(conn.stubId);
- }
- }
-}
-
-int SlotGuard::InsertConnection(SignalStub& stub, int stubId) {
- int size = static_cast<int>(mConnections.size());
- for (int i = 0; i < size; ++i) {
- auto& conn = mConnections[i];
- if (!conn.stub) {
- conn.stub = &stub;
- conn.stubId = stubId;
- return i;
- }
- }
-
- mConnections.push_back(Connection{});
- auto& conn = mConnections.back();
- conn.stub = &stub;
- conn.stubId = stubId;
- return size;
-}
-
-void SlotGuard::RemoveConnectionFor(SignalStub& stub) {
- for (auto& conn : mConnections) {
- if (conn.stub == &stub) {
- conn.stub->RemoveConnection(conn.stubId);
- }
- }
-}
-
-void SlotGuard::RemoveConnection(int slotId) {
- mConnections[slotId] = {};
-}
-
-TEST_CASE("Signal connect and disconnect") {
- Signal<> sig;
-
- int counter = 0;
- int id = sig.Connect([&]() { counter++; });
-
- sig();
- CHECK(counter == 1);
-
- sig();
- CHECK(counter == 2);
-
- sig.Disconnect(id);
- sig();
- CHECK(counter == 2);
-}
-
-TEST_CASE("Signal with parameters") {
- Signal<int> sig;
-
- int counter = 0;
- int id = sig.Connect([&](int i) { counter += i; });
-
- sig(1);
- CHECK(counter == 1);
-
- sig(0);
- CHECK(counter == 1);
-
- sig(4);
- CHECK(counter == 5);
-
- sig.Disconnect(id);
- sig(1);
- CHECK(counter == 5);
-}
-
-TEST_CASE("Signal disconnectAll()") {
- Signal<> sig;
-
- int counter1 = 0;
- int counter2 = 0;
- sig.Connect([&]() { counter1++; });
- sig.Connect([&]() { counter2++; });
-
- sig();
- CHECK(counter1 == 1);
- CHECK(counter2 == 1);
-
- sig();
- CHECK(counter1 == 2);
- CHECK(counter2 == 2);
-
- sig.DisconnectAll();
- sig();
- CHECK(counter1 == 2);
- CHECK(counter2 == 2);
-}
-
-TEST_CASE("SlotGuard auto-disconnection") {
- int counter1 = 0;
- int counter2 = 0;
- Signal<> sig;
-
- {
- SlotGuard guard;
- sig.Connect(guard, [&]() { counter1 += 1; });
- sig.Connect(guard, [&]() { counter2 += 1; });
-
- sig();
- CHECK(counter1 == 1);
- CHECK(counter2 == 1);
-
- sig();
- CHECK(counter1 == 2);
- CHECK(counter2 == 2);
- }
-
- sig();
- CHECK(counter1 == 2);
- CHECK(counter2 == 2);
-}
-
-TEST_CASE("Signal destruct before SlotGuard") {
- int counter = 0;
- SlotGuard guard;
-
- {
- Signal<> sig2;
- sig2.Connect(guard, [&]() { counter++; });
-
- sig2();
- CHECK(counter == 1);
- }
-
- // Shouldn't error
- guard.DisconnectAll();
-}
+#include "Sigslot.hpp" + +#include <doctest/doctest.h> + +bool SignalStub::Connection::IsOccupied() const { + return id != InvalidId; +} + +SignalStub::SignalStub(IWrapper& wrapper) + : mWrapper{ &wrapper } { +} + +SignalStub::~SignalStub() { + RemoveAllConnections(); +} + +std::span<const SignalStub::Connection> SignalStub::GetConnections() const { + return mConnections; +} + +SignalStub::Connection& SignalStub::InsertConnection(SlotGuard* guard) { + Connection* result; + int size = static_cast<int>(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.IsOccupied()) { + result = &conn; + result->id = i; + goto setup; + } + } + + mConnections.push_back(Connection{}); + result = &mConnections.back(); + result->id = size; + +setup: + if (guard) { + result->guard = guard; + result->slotId = guard->InsertConnection(*this, result->id); + } + return *result; +} + +void SignalStub::RemoveConnection(int id) { + if (id >= 0 && id < mConnections.size()) { + auto& conn = mConnections[id]; + if (conn.IsOccupied()) { + mWrapper->RemoveFunction(conn.id); + if (conn.guard) { + conn.guard->RemoveConnection(conn.slotId); + } + + conn.guard = nullptr; + conn.slotId = SignalStub::InvalidId; + conn.id = SignalStub::InvalidId; + } + } +} + +void SignalStub::RemoveConnectionFor(SlotGuard& guard) { + guard.RemoveConnectionFor(*this); +} + +void SignalStub::RemoveAllConnections() { + for (size_t i = 0; i < mConnections.size(); ++i) { + RemoveConnection(i); + } +} + +SlotGuard::SlotGuard() { +} + +SlotGuard::~SlotGuard() { + DisconnectAll(); +} + +void SlotGuard::DisconnectAll() { + for (auto& conn : mConnections) { + if (conn.stub) { + // Also calls SlotGuard::removeConnection, our copy of the data will be cleared in it + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +int SlotGuard::InsertConnection(SignalStub& stub, int stubId) { + int size = static_cast<int>(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.stub) { + conn.stub = &stub; + conn.stubId = stubId; + return i; + } + } + + mConnections.push_back(Connection{}); + auto& conn = mConnections.back(); + conn.stub = &stub; + conn.stubId = stubId; + return size; +} + +void SlotGuard::RemoveConnectionFor(SignalStub& stub) { + for (auto& conn : mConnections) { + if (conn.stub == &stub) { + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +void SlotGuard::RemoveConnection(int slotId) { + mConnections[slotId] = {}; +} + +TEST_CASE("Signal connect and disconnect") { + Signal<> sig; + + int counter = 0; + int id = sig.Connect([&]() { counter++; }); + + sig(); + CHECK(counter == 1); + + sig(); + CHECK(counter == 2); + + sig.Disconnect(id); + sig(); + CHECK(counter == 2); +} + +TEST_CASE("Signal with parameters") { + Signal<int> sig; + + int counter = 0; + int id = sig.Connect([&](int i) { counter += i; }); + + sig(1); + CHECK(counter == 1); + + sig(0); + CHECK(counter == 1); + + sig(4); + CHECK(counter == 5); + + sig.Disconnect(id); + sig(1); + CHECK(counter == 5); +} + +TEST_CASE("Signal disconnectAll()") { + Signal<> sig; + + int counter1 = 0; + int counter2 = 0; + sig.Connect([&]() { counter1++; }); + sig.Connect([&]() { counter2++; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + + sig.DisconnectAll(); + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("SlotGuard auto-disconnection") { + int counter1 = 0; + int counter2 = 0; + Signal<> sig; + + { + SlotGuard guard; + sig.Connect(guard, [&]() { counter1 += 1; }); + sig.Connect(guard, [&]() { counter2 += 1; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + } + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("Signal destruct before SlotGuard") { + int counter = 0; + SlotGuard guard; + + { + Signal<> sig2; + sig2.Connect(guard, [&]() { counter++; }); + + sig2(); + CHECK(counter == 1); + } + + // Shouldn't error + guard.DisconnectAll(); +} diff --git a/core/src/Utils/Sigslot.hpp b/core/src/Utils/Sigslot.hpp index 9aa5f4b..2751d9a 100644 --- a/core/src/Utils/Sigslot.hpp +++ b/core/src/Utils/Sigslot.hpp @@ -1,150 +1,150 @@ -#pragma once
-
-#include "Utils/fwd.hpp"
-
-#include <cstddef>
-#include <functional>
-#include <span>
-#include <utility>
-#include <vector>
-
-class SignalStub {
-public:
- /// Non-template interface for Signal<T...> to implement (a barrier to stop template
- /// arguments propagation).
- class IWrapper {
- public:
- virtual ~IWrapper() = default;
- virtual void RemoveFunction(int id) = 0;
- };
-
- enum {
- InvalidId = -1,
- };
-
- struct Connection {
- SlotGuard* guard;
- int slotId;
- int id = InvalidId; // If `InvalidId`, then this "spot" is unused
-
- bool IsOccupied() const;
- };
-
-private:
- std::vector<Connection> mConnections;
- IWrapper* mWrapper;
-
-private:
- template <class...>
- friend class Signal;
- friend class SlotGuard;
-
- SignalStub(IWrapper& wrapper);
- ~SignalStub();
-
- SignalStub(const SignalStub&) = delete;
- SignalStub& operator=(const SignalStub&) = delete;
- SignalStub(SignalStub&&) = default;
- SignalStub& operator=(SignalStub&&) = default;
-
- std::span<const Connection> GetConnections() const;
- Connection& InsertConnection(SlotGuard* guard = nullptr);
- void RemoveConnection(int id);
- void RemoveConnectionFor(SlotGuard& guard);
- void RemoveAllConnections();
-};
-
-template <class... TArgs>
-class Signal : public SignalStub::IWrapper {
-private:
- // Must be in this order so that mFunctions is still intact when mStub's destructor runs
- std::vector<std::function<void(TArgs...)>> mFunctions;
- SignalStub mStub;
-
-public:
- Signal()
- : mStub(*this) {
- }
-
- virtual ~Signal() = default;
-
- Signal(const Signal&) = delete;
- Signal& operator=(const Signal&) = delete;
- Signal(Signal&&) = default;
- Signal& operator=(Signal&&) = default;
-
- void operator()(TArgs... args) {
- for (auto& conn : mStub.GetConnections()) {
- if (conn.IsOccupied()) {
- mFunctions[conn.id](std::forward<TArgs>(args)...);
- }
- }
- }
-
- template <class TFunction>
- int Connect(TFunction slot) {
- auto& conn = mStub.InsertConnection();
- mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1));
- mFunctions[conn.id] = std::move(slot);
- return conn.id;
- }
-
- template <class TFunction>
- int Connect(SlotGuard& guard, TFunction slot) {
- auto& conn = mStub.InsertConnection(&guard);
- mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1));
- mFunctions[conn.id] = std::move(slot);
- return conn.id;
- }
-
- void Disconnect(int id) {
- mStub.RemoveConnection(id);
- }
-
- void DisconnectFor(SlotGuard& guard) {
- mStub.RemoveConnectionFor(guard);
- }
-
- void DisconnectAll() {
- mStub.RemoveAllConnections();
- }
-
- virtual void RemoveFunction(int id) {
- mFunctions[id] = {};
- }
-};
-
-/// Automatic disconnection mechanism for Signal<>.
-/// Bind connection to this guard by using the Connect(SlotGuard&, TFunction) overload.
-/// Either DisconnectAll() or the destructor disconnects all connections bound to this guard.
-class SlotGuard {
-private:
- struct Connection {
- SignalStub* stub = nullptr;
- int stubId = SignalStub::InvalidId;
- };
- std::vector<Connection> mConnections;
-
-public:
- friend class SignalStub;
- SlotGuard();
- ~SlotGuard();
-
- SlotGuard(const SlotGuard&) = delete;
- SlotGuard& operator=(const SlotGuard&) = delete;
- SlotGuard(SlotGuard&&) = default;
- SlotGuard& operator=(SlotGuard&&) = default;
-
- /// Disconnect all connection associated with this SlotGuard.
- void DisconnectAll();
-
-private:
- /// \return Slot id.
- int InsertConnection(SignalStub& stub, int stubId);
- /// Remove the connection data in this associated with slotId. This does not invoke
- /// the connections' stub's RemoveConnection function.
- void RemoveConnection(int slotId);
- /// Disconnect all connections from the given stub associated with this SlotGuard.
- /// Implementation for SignalStub::RemoveConnectionsFor(SlotGuard&)
- void RemoveConnectionFor(SignalStub& stub);
-};
+#pragma once + +#include "Utils/fwd.hpp" + +#include <cstddef> +#include <functional> +#include <span> +#include <utility> +#include <vector> + +class SignalStub { +public: + /// Non-template interface for Signal<T...> to implement (a barrier to stop template + /// arguments propagation). + class IWrapper { + public: + virtual ~IWrapper() = default; + virtual void RemoveFunction(int id) = 0; + }; + + enum { + InvalidId = -1, + }; + + struct Connection { + SlotGuard* guard; + int slotId; + int id = InvalidId; // If `InvalidId`, then this "spot" is unused + + bool IsOccupied() const; + }; + +private: + std::vector<Connection> mConnections; + IWrapper* mWrapper; + +private: + template <class...> + friend class Signal; + friend class SlotGuard; + + SignalStub(IWrapper& wrapper); + ~SignalStub(); + + SignalStub(const SignalStub&) = delete; + SignalStub& operator=(const SignalStub&) = delete; + SignalStub(SignalStub&&) = default; + SignalStub& operator=(SignalStub&&) = default; + + std::span<const Connection> GetConnections() const; + Connection& InsertConnection(SlotGuard* guard = nullptr); + void RemoveConnection(int id); + void RemoveConnectionFor(SlotGuard& guard); + void RemoveAllConnections(); +}; + +template <class... TArgs> +class Signal : public SignalStub::IWrapper { +private: + // Must be in this order so that mFunctions is still intact when mStub's destructor runs + std::vector<std::function<void(TArgs...)>> mFunctions; + SignalStub mStub; + +public: + Signal() + : mStub(*this) { + } + + virtual ~Signal() = default; + + Signal(const Signal&) = delete; + Signal& operator=(const Signal&) = delete; + Signal(Signal&&) = default; + Signal& operator=(Signal&&) = default; + + void operator()(TArgs... args) { + for (auto& conn : mStub.GetConnections()) { + if (conn.IsOccupied()) { + mFunctions[conn.id](std::forward<TArgs>(args)...); + } + } + } + + template <class TFunction> + int Connect(TFunction slot) { + auto& conn = mStub.InsertConnection(); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + template <class TFunction> + int Connect(SlotGuard& guard, TFunction slot) { + auto& conn = mStub.InsertConnection(&guard); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + void Disconnect(int id) { + mStub.RemoveConnection(id); + } + + void DisconnectFor(SlotGuard& guard) { + mStub.RemoveConnectionFor(guard); + } + + void DisconnectAll() { + mStub.RemoveAllConnections(); + } + + virtual void RemoveFunction(int id) { + mFunctions[id] = {}; + } +}; + +/// Automatic disconnection mechanism for Signal<>. +/// Bind connection to this guard by using the Connect(SlotGuard&, TFunction) overload. +/// Either DisconnectAll() or the destructor disconnects all connections bound to this guard. +class SlotGuard { +private: + struct Connection { + SignalStub* stub = nullptr; + int stubId = SignalStub::InvalidId; + }; + std::vector<Connection> mConnections; + +public: + friend class SignalStub; + SlotGuard(); + ~SlotGuard(); + + SlotGuard(const SlotGuard&) = delete; + SlotGuard& operator=(const SlotGuard&) = delete; + SlotGuard(SlotGuard&&) = default; + SlotGuard& operator=(SlotGuard&&) = default; + + /// Disconnect all connection associated with this SlotGuard. + void DisconnectAll(); + +private: + /// \return Slot id. + int InsertConnection(SignalStub& stub, int stubId); + /// Remove the connection data in this associated with slotId. This does not invoke + /// the connections' stub's RemoveConnection function. + void RemoveConnection(int slotId); + /// Disconnect all connections from the given stub associated with this SlotGuard. + /// Implementation for SignalStub::RemoveConnectionsFor(SlotGuard&) + void RemoveConnectionFor(SignalStub& stub); +}; diff --git a/core/src/Utils/String.cpp b/core/src/Utils/String.cpp deleted file mode 100644 index 94cd0f5..0000000 --- a/core/src/Utils/String.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "String.hpp"
-
-#include <doctest/doctest.h>
-
-Utf8Iterator::Utf8Iterator(std::string_view::iterator it)
- : mIter{ std::move(it) } {
-}
-
-constexpr unsigned char kFirstBitMask = 0b10000000;
-constexpr unsigned char kSecondBitMask = 0b01000000;
-constexpr unsigned char kThirdBitMask = 0b00100000;
-constexpr unsigned char kFourthBitMask = 0b00010000;
-constexpr unsigned char kFifthBitMask = 0b00001000;
-
-Utf8Iterator& Utf8Iterator::operator++() {
- char firstByte = *mIter;
- std::string::difference_type offset = 1;
-
- // This means the first byte has a value greater than 127, and so is beyond the ASCII range.
- if (firstByte & kFirstBitMask) {
- // This means that the first byte has a value greater than 224, and so it must be at least a three-octet code point.
- if (firstByte & kThirdBitMask) {
- // This means that the first byte has a value greater than 240, and so it must be a four-octet code point.
- if (firstByte & kFourthBitMask) {
- offset = 4;
- } else {
- offset = 3;
- }
- } else {
- offset = 2;
- }
- }
-
- mIter += offset;
- mDirty = true;
- return *this;
-}
-
-Utf8Iterator Utf8Iterator::operator++(int) {
- Utf8Iterator temp = *this;
- ++(*this);
- return temp;
-}
-
-Utf8Iterator& Utf8Iterator::operator--() {
- --mIter;
-
- // This means that the previous byte is not an ASCII character.
- if (*mIter & kFirstBitMask) {
- --mIter;
- if ((*mIter & kSecondBitMask) == 0) {
- --mIter;
- if ((*mIter & kSecondBitMask) == 0) {
- --mIter;
- }
- }
- }
-
- mDirty = true;
- return *this;
-}
-
-Utf8Iterator Utf8Iterator::operator--(int) {
- Utf8Iterator temp = *this;
- --(*this);
- return temp;
-}
-
-char32_t Utf8Iterator::operator*() const {
- UpdateCurrentValue();
- return mCurrentCodePoint;
-}
-
-std::string_view::iterator Utf8Iterator::AsInternal() const {
- // updateCurrentValue();
- return mIter;
-}
-
-bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs) {
- return lhs.mIter == rhs.mIter;
-}
-
-bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs) {
- return lhs.mIter != rhs.mIter;
-}
-
-bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs) {
- return lhs.mIter == rhs;
-}
-
-bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs) {
- return lhs.mIter != rhs;
-}
-
-void Utf8Iterator::UpdateCurrentValue() const {
- if (!mDirty) {
- return;
- }
-
- mCurrentCodePoint = 0;
- char firstByte = *mIter;
-
- // This means the first byte has a value greater than 127, and so is beyond the ASCII range.
- if (firstByte & kFirstBitMask) {
- // This means that the first byte has a value greater than 191, and so it must be at least a three-octet code point.
- if (firstByte & kThirdBitMask) {
- // This means that the first byte has a value greater than 224, and so it must be a four-octet code point.
- if (firstByte & kFourthBitMask) {
- mCurrentCodePoint = (firstByte & 0x07) << 18;
- char secondByte = *(mIter + 1);
- mCurrentCodePoint += (secondByte & 0x3f) << 12;
- char thirdByte = *(mIter + 2);
- mCurrentCodePoint += (thirdByte & 0x3f) << 6;
-
- char fourthByte = *(mIter + 3);
- mCurrentCodePoint += (fourthByte & 0x3f);
- } else {
- mCurrentCodePoint = (firstByte & 0x0f) << 12;
- char secondByte = *(mIter + 1);
- mCurrentCodePoint += (secondByte & 0x3f) << 6;
- char thirdByte = *(mIter + 2);
- mCurrentCodePoint += (thirdByte & 0x3f);
- }
- } else {
- mCurrentCodePoint = (firstByte & 0x1f) << 6;
- char secondByte = *(mIter + 1);
- mCurrentCodePoint += (secondByte & 0x3f);
- }
- } else {
- mCurrentCodePoint = firstByte;
- }
-
- mDirty = true;
-}
-
-Utf8IterableString::Utf8IterableString(std::string_view str)
- : mStr{ str } {
-}
-
-Utf8Iterator Utf8IterableString::begin() const {
- return Utf8Iterator(mStr.begin());
-}
-
-Utf8Iterator Utf8IterableString::end() const {
- return Utf8Iterator(mStr.end());
-}
-
-TEST_CASE("Iterating ASCII string") {
- std::string ascii("This is an ASCII string");
- std::u32string output;
- output.reserve(ascii.length());
-
- for (char32_t c : Utf8IterableString(ascii)) {
- output += c;
- }
-
- CHECK(output == U"This is an ASCII string");
-}
-
-// BMP: Basic Multilingual Plane
-TEST_CASE("Iterating BMP string") {
- std::string unicode("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
- std::u32string output;
- output.reserve(10);
-
- for (char32_t c : Utf8IterableString(unicode)) {
- output += c;
- }
-
- CHECK(output == U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
-}
-
-std::u32string ConvertUtf8To32(std::string_view in) {
- std::u32string str;
- // Actual size cannot be smaller than this
- str.reserve(in.size());
- for (char32_t codepoint : Utf8IterableString(in)) {
- str += codepoint;
- }
- return str;
-}
-
-std::string ConvertUtf32To8(std::u32string_view in) {
- std::string str;
- for (char32_t codepoint : in) {
- if (codepoint <= 0x7F) {
- str += codepoint;
- } else if (codepoint <= 0x7FF) {
- str += 0xC0 | (codepoint >> 6); // 110xxxxx
- str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
- } else if (codepoint <= 0xFFFF) {
- str += 0xE0 | (codepoint >> 12); // 1110xxxx
- str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx
- str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
- } else if (codepoint <= 0x10FFFF) {
- str += 0xF0 | (codepoint >> 18); // 11110xxx
- str += 0x80 | ((codepoint >> 12) & 0x3F); // 10xxxxxx
- str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx
- str += 0x80 | (codepoint & 0x3F); // 10xxxxxx
- }
- }
- return str;
-}
-
-TEST_CASE("convertUtf32To8() with ASCII") {
- auto output = ConvertUtf32To8(U"This is an ASCII string");
- CHECK(output == "This is an ASCII string");
-}
-
-TEST_CASE("convertUtf32To8() with BMP codepoints") {
- auto output = ConvertUtf32To8(U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
- CHECK(output == "Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
-}
-
-std::string_view StringRange(std::string_view str, size_t begin, size_t end) {
- const char* resBegin;
- size_t resLength = 0;
-
- Utf8Iterator it{ str.begin() };
- size_t i = 0; // Nth codepoint on the string
-
- // Skip until `it` points to the `begin`-th codepoint in the string
- while (i < begin) {
- i++;
- it++;
- } // Postcondition: i == begin
- resBegin = &*it.AsInternal();
-
- while (i < end) {
- auto prev = it;
- i++;
- it++;
-
- resLength += std::distance(prev.AsInternal(), it.AsInternal());
- } // Postcondition: i == end
-
- return { resBegin, resLength };
-}
-
-TEST_CASE("stringRange() with ASCII") {
- auto a = StringRange("This is an ASCII string", 1, 1 + 5);
- std::string range(a);
- CHECK(range == "his i");
-}
-
-TEST_CASE("stringRange() with BMP codepoints") {
- std::string range(StringRange("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 11, 11 + 5));
- CHECK(range == "t \u8FD9\u662F\u4E00");
-}
-
-size_t StringLength(std::string_view str) {
- size_t result = 0;
- for (char32_t _ : Utf8IterableString(str)) {
- result++;
- }
- return result;
-}
-
-TEST_CASE("StringLength() test") {
- CHECK(StringLength("This is an ASCII string") == 23);
- CHECK(StringLength("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32") == 23);
-}
-
-CodepointInfo StringLastCodepoint(std::string_view str) {
- Utf8Iterator it{ str.begin() };
- Utf8Iterator prev{ it };
- size_t codepoints = 0;
-
- Utf8Iterator end{ str.end() };
- while (it != end) {
- codepoints++;
-
- prev = it;
- it++;
- }
- // it == end
- // prev == <last codepoint in str>
-
- return {
- codepoints - 1,
- (size_t)std::distance(str.begin(), prev.AsInternal()),
- };
-}
-
-TEST_CASE("stringLastCodepoint() ASCII test") {
- auto [index, byteOffset] = StringLastCodepoint("This is an ASCII string");
- CHECK(index == 22);
- CHECK(index == 22);
-}
-
-TEST_CASE("stringLastCodepoint() BMP test") {
- auto [index, byteOffset] = StringLastCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32");
- CHECK(index == 22);
- CHECK(byteOffset == 40);
-}
-
-CodepointInfo StringCodepoint(std::string_view str, size_t codepointIdx) {
- Utf8Iterator it{ str.begin() };
- Utf8Iterator prev{ it };
- size_t codepoint = 0;
-
- Utf8Iterator end{ str.end() };
- while (true) {
- if (codepoint == codepointIdx) {
- return { codepoint, (size_t)std::distance(str.begin(), it.AsInternal()) };
- }
- if (it == end) {
- return { codepoint - 1, (size_t)std::distance(str.begin(), prev.AsInternal()) };
- }
-
- codepoint++;
-
- prev = it;
- it++;
- }
-}
-
-TEST_CASE("stringCodepoint() ASCII test") {
- auto [codepointOffset, byteOffset] = StringCodepoint("This is an ASCII string", 6);
- CHECK(codepointOffset == 6);
- CHECK(byteOffset == 6);
-}
-
-TEST_CASE("stringCodepoint() ASCII past-the-end test") {
- auto [codepointOffset, byteOffset] = StringCodepoint("This is an ASCII string", 100);
- CHECK(codepointOffset == 22);
- CHECK(byteOffset == 22);
-}
-
-TEST_CASE("stringCodepoint() BMP test") {
- auto [codepointOffset, byteOffset] = StringCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 14);
- CHECK(codepointOffset == 14);
- CHECK(byteOffset == 16);
-}
-
-TEST_CASE("stringCodepoint() BMP past-the-end test") {
- auto [codepointOffset, byteOffset] = StringCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 100);
- CHECK(codepointOffset == 22);
- CHECK(byteOffset == 40);
-}
diff --git a/core/src/Utils/String.hpp b/core/src/Utils/String.hpp deleted file mode 100644 index f2829d7..0000000 --- a/core/src/Utils/String.hpp +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once
-
-#include <cstddef>
-#include <string>
-#include <string_view>
-
-class Utf8Iterator {
-public:
- using iterator_category = std::bidirectional_iterator_tag;
- using value_type = char32_t;
- using difference_type = std::string_view::difference_type;
- using pointer = const char32_t*;
- using reference = const char32_t&;
-
-private:
- std::string_view::iterator mIter;
- mutable char32_t mCurrentCodePoint = 0;
- mutable bool mDirty = true;
-
-public:
- Utf8Iterator(std::string_view::iterator it);
- ~Utf8Iterator() = default;
-
- Utf8Iterator(const Utf8Iterator& that) = default;
- Utf8Iterator& operator=(const Utf8Iterator& that) = default;
- Utf8Iterator(Utf8Iterator&& that) = default;
- Utf8Iterator& operator=(Utf8Iterator&& that) = default;
-
- Utf8Iterator& operator++();
- Utf8Iterator operator++(int);
- Utf8Iterator& operator--();
- Utf8Iterator operator--(int);
-
- char32_t operator*() const;
- std::string_view::iterator AsInternal() const;
-
- friend bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs);
- friend bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs);
- friend bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs);
- friend bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs);
-
-private:
- void UpdateCurrentValue() const;
-};
-
-class Utf8IterableString {
-private:
- std::string_view mStr;
-
-public:
- Utf8IterableString(std::string_view str);
- Utf8Iterator begin() const;
- Utf8Iterator end() const;
-};
-
-struct StringEqual {
- using is_transparent = std::true_type;
- bool operator()(std::string_view l, std::string_view r) const noexcept { return l == r; }
-};
-struct StringHash {
- using is_transparent = std::true_type;
- auto operator()(std::string_view str) const noexcept { return std::hash<std::string_view>{}(str); }
-};
-
-std::u32string ConvertUtf8To32(std::string_view str);
-std::string ConvertUtf32To8(std::u32string_view str);
-
-/// Slice the given UTF-8 string into the given range, in codepoints.
-std::string_view StringRange(std::string_view str, size_t begin, size_t end);
-
-/// Calculate the given UTF-8 string's number of codepoints.
-size_t StringLength(std::string_view str);
-
-struct CodepointInfo {
- size_t index;
- size_t byteOffset;
-};
-
-/// Find info about the last codepoint in the given UTF-8 string.
-/// \param str A non-empty UTF-8 encoded string.
-CodepointInfo StringLastCodepoint(std::string_view str);
-/// Find info about the nth codepoint in the given UTF-8 string. If codepointIdx is larger than the length, info for the last codepoint will be returned.
-/// \param str A non-empty UTF-8 encoded string.
-CodepointInfo StringCodepoint(std::string_view str, size_t codepointIdx);
diff --git a/core/src/Utils/fwd.hpp b/core/src/Utils/fwd.hpp index f33cb14..58b2991 100644 --- a/core/src/Utils/fwd.hpp +++ b/core/src/Utils/fwd.hpp @@ -1,20 +1,20 @@ -#pragma once
-
-// Sigslot.hpp
-class SignalStub;
-template <class... TArgs>
-class Signal;
-class SlotGuard;
-
-// I18n.hpp
-class I18n;
-struct StringArgument;
-struct IntArgument;
-struct FloatArgument;
-class BasicTranslation;
-class FormattedTranslation;
-class NumericTranslation;
-
-// String.hpp
-class Utf8Iterator;
-class Utf8IterableString;
+#pragma once + +// Sigslot.hpp +class SignalStub; +template <class... TArgs> +class Signal; +class SlotGuard; + +// I18n.hpp +class I18n; +struct StringArgument; +struct IntArgument; +struct FloatArgument; +class BasicTranslation; +class FormattedTranslation; +class NumericTranslation; + +// String.hpp +class Utf8Iterator; +class Utf8IterableString; |