diff options
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/Entrypoint/main.cpp | 4 | ||||
-rw-r--r-- | core/src/Model/GlobalStates.cpp | 110 | ||||
-rw-r--r-- | core/src/Model/GlobalStates.hpp | 43 | ||||
-rw-r--r-- | core/src/Model/Project.cpp | 50 | ||||
-rw-r--r-- | core/src/Model/Project.hpp | 9 | ||||
-rw-r--r-- | core/src/Model/fwd.hpp | 3 | ||||
-rw-r--r-- | core/src/UI/Localization.hpp | 20 | ||||
-rw-r--r-- | core/src/UI/States.cpp | 15 | ||||
-rw-r--r-- | core/src/UI/States.hpp | 6 | ||||
-rw-r--r-- | core/src/UI/UI.hpp | 7 | ||||
-rw-r--r-- | core/src/UI/UI_MainWindow.cpp | 129 | ||||
-rw-r--r-- | core/src/UI/UI_Utils.cpp | 16 | ||||
-rw-r--r-- | core/src/Utils/StandardDirectories.cpp | 74 | ||||
-rw-r--r-- | core/src/Utils/StandardDirectories.hpp | 10 |
14 files changed, 485 insertions, 11 deletions
diff --git a/core/src/Entrypoint/main.cpp b/core/src/Entrypoint/main.cpp index 39943d6..ce8aab7 100644 --- a/core/src/Entrypoint/main.cpp +++ b/core/src/Entrypoint/main.cpp @@ -5,6 +5,7 @@ #include "Entrypoint/OpenGL2.hpp" #include "Entrypoint/OpenGL3.hpp" #include "Entrypoint/Vulkan.hpp" +#include "Model/GlobalStates.hpp" #include "UI/Localization.hpp" #include "UI/States.hpp" #include "UI/UI.hpp" @@ -143,6 +144,7 @@ int main(int argc, char* argv[]) { I18n::Init(); I18n::SetLanguage("zh_CN"); + GlobalStates::Init(); UIState::Init(); auto window = backend->GetWindow(); @@ -152,5 +154,7 @@ int main(int argc, char* argv[]) { backend->EndFrame(); } + GlobalStates::Shutdown(); + return 0; } diff --git a/core/src/Model/GlobalStates.cpp b/core/src/Model/GlobalStates.cpp new file mode 100644 index 0000000..cd076f4 --- /dev/null +++ b/core/src/Model/GlobalStates.cpp @@ -0,0 +1,110 @@ +#include "GlobalStates.hpp" + +#include "Model/Project.hpp" +#include "Utils/StandardDirectories.hpp" + +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <cassert> +#include <filesystem> +#include <fstream> +#include <memory> + +namespace fs = std::filesystem; + +static std::unique_ptr<GlobalStates> globalStateInstance; +static fs::path globalDataPath; + +void GlobalStates::Init() { + globalStateInstance = std::make_unique<GlobalStates>(); + globalDataPath = StandardDirectories::UserData() / "cplt"; + fs::create_directories(globalDataPath); + + // Reading recent projects + [&]() { + std::ifstream ifs(globalDataPath / "recents.json"); + if (!ifs) return; + + Json::Value root; + ifs >> root; + + if (!root.isObject()) return; + if (auto& recents = root["RecentProjects"]; recents.isArray()) { + for (auto& elm : recents) { + if (!elm.isString()) continue; + + fs::path path(elm.asCString()); + if (!fs::exists(path)) continue; + + auto utf8String = path.string(); + globalStateInstance->mRecentProjects.push_back(RecentProject{ + .path = std::move(path), + .cachedUtf8String = std::move(utf8String), + }); + } + } + }(); +} + +void GlobalStates::Shutdown() { + if (!globalStateInstance) return; + if (globalStateInstance->mDirty) { + globalStateInstance->WriteToDisk(); + } +} + +GlobalStates& GlobalStates::GetInstance() { + return *globalStateInstance; +} + +const std::filesystem::path& GlobalStates::GetUserDataPath() { + return globalDataPath; +} + +const std::vector<GlobalStates::RecentProject>& GlobalStates::GetRecentProjects() const { + return mRecentProjects; +} + +void GlobalStates::ClearRecentProjects() { + mRecentProjects.clear(); + MarkDirty(); +} + +void GlobalStates::AddRecentProject(const Project& project) { + mRecentProjects.push_back(RecentProject{ + .path = project.GetPath(), + .cachedUtf8String = project.GetPath().string(), + }); + MarkDirty(); +} + +void GlobalStates::RemoveRecentProject(int idx) { + assert(idx >= 0 && idx < mRecentProjects.size()); + + mRecentProjects.erase(mRecentProjects.begin() + idx); + MarkDirty(); +} + +void GlobalStates::WriteToDisk() const { + Json::Value root; + + auto& recentProjects = root["RecentProjects"] = Json::Value(Json::arrayValue); + for (auto& [path, _] : mRecentProjects) { + recentProjects.append(Json::Value(path.string())); + } + + std::ofstream ofs(globalDataPath / "recents.json"); + ofs << root; + + mDirty = false; +} + +bool GlobalStates::IsDirty() const { + return mDirty; +} + +void GlobalStates::MarkDirty() { + mDirty = true; + OnModified(); +} diff --git a/core/src/Model/GlobalStates.hpp b/core/src/Model/GlobalStates.hpp new file mode 100644 index 0000000..e6d823b --- /dev/null +++ b/core/src/Model/GlobalStates.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "Utils/Sigslot.hpp" +#include "cplt_fwd.hpp" + +#include <filesystem> +#include <string> +#include <vector> + +class GlobalStates { +public: + static void Init(); + static void Shutdown(); + + static GlobalStates& GetInstance(); + static const std::filesystem::path& GetUserDataPath(); + + struct RecentProject { + std::filesystem::path path; + std::string cachedUtf8String; + }; + +public: + Signal<> OnModified; + +private: + std::vector<RecentProject> mRecentProjects; + mutable bool mDirty = false; + +public: + const std::vector<RecentProject>& GetRecentProjects() const; + void ClearRecentProjects(); + void AddRecentProject(const Project& project); + void RemoveRecentProject(int idx); + + // TODO async autosaving to prevent data loss on crash + void WriteToDisk() const; + + bool IsDirty() const; + +private: + void MarkDirty(); +}; diff --git a/core/src/Model/Project.cpp b/core/src/Model/Project.cpp index f301bb8..c54a02c 100644 --- a/core/src/Model/Project.cpp +++ b/core/src/Model/Project.cpp @@ -1,8 +1,56 @@ #include "Project.hpp" +#include <json/reader.h> +#include <json/value.h> +#include <json/writer.h> +#include <filesystem> +#include <fstream> +#include <stdexcept> #include <utility> -const std::filesystem::path& Project::GetPath() const { +namespace fs = std::filesystem; + +Project Project::Load(const fs::path& path) { + // TODO better diagnostic + const char* kInvalidFormatErr = "Failed to load project: invalid format."; + + std::ifstream ifs(path); + if (!ifs) { + std::string message; + message += "Failed to load project file at '"; + message += path.string(); + message += "'."; + throw std::runtime_error(message); + } + + Project proj; + proj.mRootPath = path.parent_path(); + + 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 siliently 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); + } + + return proj; +} + +Project Project::Create(std::string name, const fs::path& path) { + Project proj; + proj.mRootPath = path; + proj.mName = std::move(name); + return proj; +} + +const fs::path& Project::GetPath() const { return mRootPath; } diff --git a/core/src/Model/Project.hpp b/core/src/Model/Project.hpp index 34ae1d7..7b5c7e3 100644 --- a/core/src/Model/Project.hpp +++ b/core/src/Model/Project.hpp @@ -9,9 +9,18 @@ public: std::string mName; public: + /// Load the project from a cplt_project.json file. + static Project Load(const std::filesystem::path& path); + /// 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. const std::filesystem::path& GetPath() const; const std::string& GetName() const; void SetName(std::string name); + +private: + Project() = default; }; diff --git a/core/src/Model/fwd.hpp b/core/src/Model/fwd.hpp index f5a5818..6bbc0b7 100644 --- a/core/src/Model/fwd.hpp +++ b/core/src/Model/fwd.hpp @@ -1,4 +1,7 @@ #pragma once +// GlobalStates.hpp +class GlobalStates; + // Project.hpp class Project; diff --git a/core/src/UI/Localization.hpp b/core/src/UI/Localization.hpp index 648dd6d..7b401ef 100644 --- a/core/src/UI/Localization.hpp +++ b/core/src/UI/Localization.hpp @@ -18,8 +18,20 @@ public: BasicTranslation TabItems{ "MainWindow.Tab.Items"sv }; BasicTranslation TabExport{ "MainWindow.Tab.Exports"sv }; - BasicTranslation NewProject{ "Project.NewProject"sv }; - BasicTranslation OpenProject{ "Project.OpenProject"sv }; - BasicTranslation Recents{ "Project.Recents"sv }; - BasicTranslation ClearRecents{ "Project.ClearRecents"sv }; + BasicTranslation NewProject{ "Project.New"sv }; + BasicTranslation TitleNewProject{ "Project.New.DialogTitle"sv }; + BasicTranslation ActionNewProjectConfirm{ "Project.New.Confirm"sv }; + BasicTranslation ActionNewProjectCancel{ "Project.New.Cancel"sv }; + BasicTranslation HintNewProjectName{ "Project.New.Name"sv }; + BasicTranslation HintNewProjectPath{ "Project.New.Path"sv }; + BasicTranslation ErrorNewProjectEmptyName{ "Project.New.EmptyName"sv }; + BasicTranslation ErrorNewProjectInvalidPath{ "Project.New.InvalidPath"sv }; + + BasicTranslation OpenProject{ "Project.Open"sv }; + + BasicTranslation RecentProjects{ "Project.Recents"sv }; + BasicTranslation ActionClearRecentProjects{ "Project.Recents.Clear"sv }; + BasicTranslation MessageNoRecentProjects{ "Project.Recents.NonePresent"sv }; + BasicTranslation TooltipOpenRecentProject{ "Project.Recents.Open.Tooltip"sv }; + BasicTranslation TooltipDeleteRecentProject{ "Project.Recents.Delete.Tooltip"sv }; }; diff --git a/core/src/UI/States.cpp b/core/src/UI/States.cpp index efae152..07bbcf7 100644 --- a/core/src/UI/States.cpp +++ b/core/src/UI/States.cpp @@ -2,6 +2,9 @@ #include "Model/Project.hpp" +#include <memory> +#include <utility> + static std::unique_ptr<UIState> uiStateInstance; void UIState::Init() { @@ -11,3 +14,15 @@ void UIState::Init() { UIState& UIState::GetInstance() { return *uiStateInstance; } + +void UIState::SetCurrentProject(std::unique_ptr<Project> project) { + CloseCurrentProject(); + CurrentProject = std::move(project); +} + +void UIState::CloseCurrentProject() { + if (CurrentProject) { + // TODO save stuff + CurrentProject = nullptr; + } +} diff --git a/core/src/UI/States.hpp b/core/src/UI/States.hpp index d1c1faf..cbb556f 100644 --- a/core/src/UI/States.hpp +++ b/core/src/UI/States.hpp @@ -4,6 +4,8 @@ #include <memory> +/// Minimal state shared by all UI components, such as database, items, export, etc. +/// Note that global components (settings) is not supposed to access these. class UIState { public: static void Init(); @@ -11,4 +13,8 @@ public: public: std::unique_ptr<Project> CurrentProject; + +public: + void SetCurrentProject(std::unique_ptr<Project> project); + void CloseCurrentProject(); }; diff --git a/core/src/UI/UI.hpp b/core/src/UI/UI.hpp index 08f5771..b0c3aaa 100644 --- a/core/src/UI/UI.hpp +++ b/core/src/UI/UI.hpp @@ -1,5 +1,12 @@ #pragma once +namespace ImGui { + +void ErrorIcon(); +void WarningIcon(); + +} // namespace ImGui + namespace UI { void MainWindow(); diff --git a/core/src/UI/UI_MainWindow.cpp b/core/src/UI/UI_MainWindow.cpp index 0c8e7b9..9b20550 100644 --- a/core/src/UI/UI_MainWindow.cpp +++ b/core/src/UI/UI_MainWindow.cpp @@ -1,34 +1,151 @@ #include "UI.hpp" +#include "Model/GlobalStates.hpp" #include "Model/Project.hpp" #include "UI/Localization.hpp" #include "UI/States.hpp" +#include <IconsFontAwesome.h> #include <imgui.h> +#include <imgui_internal.h> +#include <imgui_stdlib.h> +#include <filesystem> + +namespace fs = std::filesystem; namespace { +void LoadProjectAt(const std::filesystem::path& path) { + auto& uis = UIState::GetInstance(); + auto& gs = GlobalStates::GetInstance(); + + if (uis.CurrentProject) { + uis.CloseCurrentProject(); + } +} + void ProjectTab_Normal() { // TODO } void ProjectTab_NoProject() { auto ls = LocaleStrings::Instance.get(); + auto& gs = GlobalStates::GetInstance(); + auto& uis = UIState::GetInstance(); + static std::string projectName; + static std::string dirName; + static fs::path dirPath; + static bool dirNameIsValid = false; if (ImGui::Button(ls->NewProject.Get())) { - // TODO + auto vs = ImGui::GetMainViewport()->Size; // Viewport Size + ImGui::SetNextWindowSize({ vs.x * 0.5f, vs.y * 0.5f }); + ImGui::SetNextWindowPos({ vs.x / 2, vs.y / 2 }, ImGuiCond_Always, { 0.5f, 0.5f }); // Center window initially + ImGui::OpenPopup(ls->TitleNewProject.Get()); } + + // Make it so that the modal dialog has a close button + bool newProjectDialogDummyTrue = true; + if (ImGui::BeginPopupModal(ls->TitleNewProject.Get(), &newProjectDialogDummyTrue)) { + ImGui::InputTextWithHint("##ProjectName", ls->HintNewProjectName.Get(), &projectName); + + if (ImGui::InputTextWithHint("##ProjectPath", ls->HintNewProjectPath.Get(), &dirName)) { + // Changed, validate value + fs::path newPath(dirName); + if (fs::exists(newPath)) { + dirNameIsValid = true; + dirPath = std::move(newPath); + } else { + dirNameIsValid = false; + } + } + ImGui::SameLine(); + if (ImGui::Button("...")) { + // TODO file dialog + } + + if (projectName.empty()) { + ImGui::ErrorIcon(); + + ImGui::SameLine(); + ImGui::Text(ls->ErrorNewProjectEmptyName.Get()); + } + + if (!dirNameIsValid) { + ImGui::ErrorIcon(); + + ImGui::SameLine(); + ImGui::Text(ls->ErrorNewProjectInvalidPath.Get()); + } + + ImGui::Spacing(); + + bool formValid = dirNameIsValid && !projectName.empty(); + + if (!formValid) { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, false); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f * ImGui::GetStyle().Alpha); + } + if (ImGui::Button(ls->ActionNewProjectConfirm.Get())) { + ImGui::CloseCurrentPopup(); + + auto project = Project::Create(std::move(projectName), dirPath); + auto uptr = std::unique_ptr<Project>(new Project(std::move(project))); + uis.SetCurrentProject(std::move(uptr)); + + // Dialog just got closed, reset states + projectName = ""; + dirName = ""; + dirPath = fs::path{}; + dirNameIsValid = false; + } + if (!formValid) { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + } + + ImGui::SameLine(); + if (ImGui::Button(ls->ActionNewProjectCancel.Get())) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + if (ImGui::Button(ls->OpenProject.Get())) { // TODO } ImGui::Separator(); - ImGui::Text(ls->Recents.Get()); + ImGui::Text(ls->RecentProjects.Get()); ImGui::SameLine(); - if (ImGui::Button(ls->ClearRecents.Get())) { - // TODO + if (ImGui::Button(ls->ActionClearRecentProjects.Get())) { + gs.ClearRecentProjects(); } - // TODO + auto& recentProjects = gs.GetRecentProjects(); + if (recentProjects.empty()) { + ImGui::Text(ls->MessageNoRecentProjects.Get()); + } + for (auto it = recentProjects.begin(); it != recentProjects.end(); ++it) { + auto& [path, recent] = *it; + ImGui::Text(recent.c_str()); + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EDIT)) { + LoadProjectAt(path); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(ls->TooltipOpenRecentProject.Get()); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TRASH)) { + gs.RemoveRecentProject(std::distance(recentProjects.begin(), it)); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(ls->TooltipDeleteRecentProject.Get()); + } + } } } // namespace @@ -46,7 +163,7 @@ void UI::MainWindow() { ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(ls->TabProject.Get())) { + if (ImGui::BeginTabItem(ls->TabProject.Get(), nullptr, ImGuiTabItemFlags_SetSelected)) { if (uis.CurrentProject) { ProjectTab_Normal(); } else { diff --git a/core/src/UI/UI_Utils.cpp b/core/src/UI/UI_Utils.cpp new file mode 100644 index 0000000..61a62f0 --- /dev/null +++ b/core/src/UI/UI_Utils.cpp @@ -0,0 +1,16 @@ +#include "UI.hpp" + +#include <IconsFontAwesome.h> +#include <imgui.h> + +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::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(); +} diff --git a/core/src/Utils/StandardDirectories.cpp b/core/src/Utils/StandardDirectories.cpp new file mode 100644 index 0000000..7defc5d --- /dev/null +++ b/core/src/Utils/StandardDirectories.cpp @@ -0,0 +1,74 @@ +#include "StandardDirectories.hpp" + +#include <filesystem> +#include <stdexcept> + +namespace fs = std::filesystem; + +#if PLATFORM_WIN32 +// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c +# include <ShlObj_core.h> +# include <objbase.h> +# pragma comment(lib, "shell32.lib") +# pragma comment(lib, "ole32.lib") + +static fs::path GetAppDataRoaming() { + PWSTR path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); + if (SUCCEEDED(hr)) { + auto dataDir = fs::path(path); + CoTaskMemFree(path); + + fs::create_directories(dataDir); + return dataDir; + } else { + fs::path dataDir("~/AppData/Roaming"); + fs::create_directories(dataDir); + return dataDir; + } +} + +#elif PLATFORM_MACOS +// TODO +#elif PLATFORM_LINUX +# include <cstdlib> + +static fs::path GetEnvVar(const char* name, const char* backup) { + if (const char* path = std::getenv(name)) { + fs::path dataDir(path); + fs::create_directories(dataDir); + return dataDir; + } else { + fs::path dataDir(backup); + fs::create_directories(dataDir); + return dataDir; + } +} + +#endif + +const std::filesystem::path& StandardDirectories::UserData() { + static auto userDataDir = []() -> fs::path { +#if PLATFORM_WIN32 + return GetAppDataRoaming(); +#elif PLATFORM_MACOS + // TODO where? +#elif PLATFORM_LINUX + return GetEnvVar("XDG_DATA_HOME", "~/.local/share"); +#endif + }(); + return userDataDir; +} + +const std::filesystem::path& StandardDirectories::UserConfig() { + static auto userConfigDir = []() -> fs::path { +#if PLATFORM_WIN32 + return GetAppDataRoaming(); +#elif PLATFORM_MACOS + // TODO where? +#elif PLATFORM_LINUX + return GetEnvVar("XDG_CONFIG_HOME", "~/.config"); +#endif + }(); + return userConfigDir; +} diff --git a/core/src/Utils/StandardDirectories.hpp b/core/src/Utils/StandardDirectories.hpp new file mode 100644 index 0000000..4f7e5e2 --- /dev/null +++ b/core/src/Utils/StandardDirectories.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <filesystem> + +namespace StandardDirectories { + +const std::filesystem::path& UserData(); +const std::filesystem::path& UserConfig(); + +} // namespace StandardDirectories |