aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/CMakeLists.txt1
-rw-r--r--core/locale/zh_CN.json31
-rw-r--r--core/src/Entrypoint/main.cpp58
-rw-r--r--core/src/Model/GlobalStates.cpp17
-rw-r--r--core/src/Model/GlobalStates.hpp5
-rw-r--r--core/src/Model/Items.cpp88
-rw-r--r--core/src/Model/Items.hpp209
-rw-r--r--core/src/Model/Project.cpp66
-rw-r--r--core/src/Model/Project.hpp6
-rw-r--r--core/src/Model/fwd.hpp1
-rw-r--r--core/src/UI/Localization.hpp32
-rw-r--r--core/src/UI/States.cpp4
-rw-r--r--core/src/UI/States.hpp1
-rw-r--r--core/src/UI/UI.hpp9
-rw-r--r--core/src/UI/UI_Items.cpp212
-rw-r--r--core/src/UI/UI_MainWindow.cpp102
-rw-r--r--core/src/UI/UI_Utils.cpp38
-rw-r--r--core/src/Utils/Enum.hpp164
-rw-r--r--core/src/Utils/I18n.cpp520
-rw-r--r--core/src/Utils/I18n.hpp149
-rw-r--r--core/src/Utils/Sigslot.cpp428
-rw-r--r--core/src/Utils/Sigslot.hpp300
-rw-r--r--core/src/Utils/String.cpp340
-rw-r--r--core/src/Utils/String.hpp84
-rw-r--r--core/src/Utils/fwd.hpp40
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;