blob: a55cf8273dfc570f66a1f4fedfbf64c4049efb6d [file] [log] [blame]
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "glass/Context.h"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <filesystem>
#include <fmt/format.h>
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_stdlib.h>
#include <wpi/StringExtras.h>
#include <wpi/fs.h>
#include <wpi/json.h>
#include <wpi/json_serializer.h>
#include <wpi/raw_istream.h>
#include <wpi/raw_ostream.h>
#include <wpi/timestamp.h>
#include <wpigui.h>
#include <wpigui_internal.h>
#include "glass/ContextInternal.h"
using namespace glass;
Context* glass::gContext;
static void WorkspaceResetImpl() {
// call reset functions
for (auto&& reset : gContext->workspaceReset) {
if (reset) {
reset();
}
}
// clear storage
for (auto&& root : gContext->storageRoots) {
root.second->Clear();
}
// ImGui reset
ImGui::ClearIniSettings();
}
static void WorkspaceInit() {
for (auto&& init : gContext->workspaceInit) {
if (init) {
init();
}
}
for (auto&& root : gContext->storageRoots) {
root.getValue()->Apply();
}
}
static bool JsonToWindow(const wpi::json& jfile, const char* filename) {
if (!jfile.is_object()) {
ImGui::LogText("%s top level is not object", filename);
return false;
}
// loop over JSON and generate ini format
std::string iniStr;
wpi::raw_string_ostream ini{iniStr};
for (auto&& jsection : jfile.items()) {
if (jsection.key() == "Docking") {
continue;
}
if (!jsection.value().is_object()) {
ImGui::LogText("%s section %s is not object", filename,
jsection.key().c_str());
return false;
}
for (auto&& jsubsection : jsection.value().items()) {
if (!jsubsection.value().is_object()) {
ImGui::LogText("%s section %s subsection %s is not object", filename,
jsection.key().c_str(), jsubsection.key().c_str());
return false;
}
ini << '[' << jsection.key() << "][" << jsubsection.key() << "]\n";
for (auto&& jkv : jsubsection.value().items()) {
try {
auto& value = jkv.value().get_ref<const std::string&>();
ini << jkv.key() << '=' << value << "\n";
} catch (wpi::json::exception&) {
ImGui::LogText("%s section %s subsection %s value %s is not string",
filename, jsection.key().c_str(),
jsubsection.key().c_str(), jkv.key().c_str());
return false;
}
}
ini << '\n';
}
}
// emit Docking section last
auto docking = jfile.find("Docking");
if (docking != jfile.end()) {
for (auto&& jsubsection : docking->items()) {
if (!jsubsection.value().is_array()) {
ImGui::LogText("%s section %s subsection %s is not array", filename,
"Docking", jsubsection.key().c_str());
return false;
}
ini << "[Docking][" << jsubsection.key() << "]\n";
for (auto&& jv : jsubsection.value()) {
try {
auto& value = jv.get_ref<const std::string&>();
ini << value << "\n";
} catch (wpi::json::exception&) {
ImGui::LogText("%s section %s subsection %s value is not string",
filename, "Docking", jsubsection.key().c_str());
return false;
}
}
ini << '\n';
}
}
ini.flush();
ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
return true;
}
static bool LoadWindowStorageImpl(const std::string& filename) {
std::error_code ec;
wpi::raw_fd_istream is{filename, ec};
if (ec) {
ImGui::LogText("error opening %s: %s", filename.c_str(),
ec.message().c_str());
return false;
} else {
try {
return JsonToWindow(wpi::json::parse(is), filename.c_str());
} catch (wpi::json::parse_error& e) {
ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
return false;
}
}
}
static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
std::string_view rootName) {
std::error_code ec;
wpi::raw_fd_istream is{filename, ec};
if (ec) {
ImGui::LogText("error opening %s: %s", filename.c_str(),
ec.message().c_str());
return false;
} else {
auto& storage = ctx->storageRoots[rootName];
bool createdStorage = false;
if (!storage) {
storage = std::make_unique<Storage>();
createdStorage = true;
}
try {
storage->FromJson(wpi::json::parse(is), filename.c_str());
} catch (wpi::json::parse_error& e) {
ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
if (createdStorage) {
ctx->storageRoots.erase(rootName);
}
return false;
}
}
return true;
}
static bool LoadStorageImpl(Context* ctx, std::string_view dir,
std::string_view name) {
WorkspaceResetImpl();
bool rv = true;
for (auto&& root : ctx->storageRoots) {
std::string filename;
auto rootName = root.getKey();
if (rootName.empty()) {
filename = (fs::path{dir} / fmt::format("{}.json", name)).string();
} else {
filename =
(fs::path{dir} / fmt::format("{}-{}.json", name, rootName)).string();
}
if (!LoadStorageRootImpl(ctx, filename, rootName)) {
rv = false;
}
}
WorkspaceInit();
return rv;
}
static wpi::json WindowToJson() {
size_t iniLen;
const char* iniData = ImGui::SaveIniSettingsToMemory(&iniLen);
std::string_view ini{iniData, iniLen};
// parse the ini data and build JSON
// JSON format:
// {
// "Section": {
// "Subsection": {
// "Key": "Value" // all values are saved as strings
// }
// }
// }
wpi::json out = wpi::json::object();
wpi::json* curSection = nullptr;
while (!ini.empty()) {
std::string_view line;
std::tie(line, ini) = wpi::split(ini, '\n');
line = wpi::trim(line);
if (line.empty()) {
continue;
}
if (line[0] == '[') {
// new section
auto [section, subsection] = wpi::split(line, ']');
section = wpi::drop_front(section); // drop '['; ']' was dropped by split
subsection = wpi::drop_back(wpi::drop_front(subsection)); // drop []
auto& jsection = out[section];
if (jsection.is_null()) {
jsection = wpi::json::object();
}
curSection = &jsection[subsection];
if (curSection->is_null()) {
if (section == "Docking") {
*curSection = wpi::json::array();
} else {
*curSection = wpi::json::object();
}
}
} else {
// value
if (!curSection) {
continue; // shouldn't happen, but just in case
}
auto [name, value] = wpi::split(line, '=');
if (curSection->is_object()) {
(*curSection)[name] = value;
} else if (curSection->is_array()) {
curSection->emplace_back(line);
}
}
}
return out;
}
bool SaveWindowStorageImpl(const std::string& filename) {
std::error_code ec;
wpi::raw_fd_ostream os{filename, ec};
if (ec) {
ImGui::LogText("error opening %s: %s", filename.c_str(),
ec.message().c_str());
return false;
}
WindowToJson().dump(os, 2);
os << '\n';
return true;
}
static bool SaveStorageRootImpl(Context* ctx, const std::string& filename,
const Storage& storage) {
std::error_code ec;
wpi::raw_fd_ostream os{filename, ec};
if (ec) {
ImGui::LogText("error opening %s: %s", filename.c_str(),
ec.message().c_str());
return false;
}
storage.ToJson().dump(os, 2);
os << '\n';
return true;
}
static bool SaveStorageImpl(Context* ctx, std::string_view dir,
std::string_view name, bool exiting) {
fs::path dirPath{dir};
std::error_code ec;
fs::create_directories(dirPath, ec);
if (ec) {
return false;
}
// handle erasing save files on exit if requested
if (exiting && wpi::gui::gContext->resetOnExit) {
fs::remove(dirPath / fmt::format("{}-window.json", name), ec);
for (auto&& root : ctx->storageRoots) {
auto rootName = root.getKey();
if (rootName.empty()) {
fs::remove(dirPath / fmt::format("{}.json", name), ec);
} else {
fs::remove(dirPath / fmt::format("{}-{}.json", name, rootName), ec);
}
}
}
bool rv = SaveWindowStorageImpl(
(dirPath / fmt::format("{}-window.json", name)).string());
for (auto&& root : ctx->storageRoots) {
auto rootName = root.getKey();
std::string filename;
if (rootName.empty()) {
filename = (dirPath / fmt::format("{}.json", name)).string();
} else {
filename = (dirPath / fmt::format("{}-{}.json", name, rootName)).string();
}
if (!SaveStorageRootImpl(ctx, filename, *root.getValue())) {
rv = false;
}
}
return rv;
}
Context::Context()
: sourceNameStorage{storageRoots.insert({"", std::make_unique<Storage>()})
.first->getValue()
->GetChild("sourceNames")} {
storageStack.emplace_back(storageRoots[""].get());
// override ImGui ini saving
wpi::gui::ConfigureCustomSaveSettings(
[this] { LoadStorageImpl(this, storageLoadDir, storageName); },
[this] {
LoadWindowStorageImpl((fs::path{storageLoadDir} /
fmt::format("{}-window.json", storageName))
.string());
},
[this](bool exiting) {
SaveStorageImpl(this, storageAutoSaveDir, storageName, exiting);
});
}
Context::~Context() {
wpi::gui::ConfigureCustomSaveSettings(nullptr, nullptr, nullptr);
}
Context* glass::CreateContext() {
Context* ctx = new Context;
if (!gContext) {
SetCurrentContext(ctx);
}
return ctx;
}
void glass::DestroyContext(Context* ctx) {
if (!ctx) {
ctx = gContext;
}
if (gContext == ctx) {
SetCurrentContext(nullptr);
}
delete ctx;
}
Context* glass::GetCurrentContext() {
return gContext;
}
void glass::SetCurrentContext(Context* ctx) {
gContext = ctx;
}
void glass::ResetTime() {
gContext->zeroTime = wpi::Now();
}
uint64_t glass::GetZeroTime() {
return gContext->zeroTime;
}
void glass::WorkspaceReset() {
WorkspaceResetImpl();
WorkspaceInit();
}
void glass::AddWorkspaceInit(std::function<void()> init) {
if (init) {
gContext->workspaceInit.emplace_back(std::move(init));
}
}
void glass::AddWorkspaceReset(std::function<void()> reset) {
if (reset) {
gContext->workspaceReset.emplace_back(std::move(reset));
}
}
void glass::SetStorageName(std::string_view name) {
gContext->storageName = name;
}
void glass::SetStorageDir(std::string_view dir) {
if (dir.empty()) {
gContext->storageLoadDir = ".";
gContext->storageAutoSaveDir = ".";
} else {
gContext->storageLoadDir = dir;
gContext->storageAutoSaveDir = dir;
gContext->isPlatformSaveDir = (dir == wpi::gui::GetPlatformSaveFileDir());
}
}
std::string glass::GetStorageDir() {
return gContext->storageAutoSaveDir;
}
bool glass::LoadStorage(std::string_view dir) {
SaveStorage();
SetStorageDir(dir);
LoadWindowStorageImpl((fs::path{gContext->storageLoadDir} /
fmt::format("{}-window.json", gContext->storageName))
.string());
return LoadStorageImpl(gContext, dir, gContext->storageName);
}
bool glass::SaveStorage() {
return SaveStorageImpl(gContext, gContext->storageAutoSaveDir,
gContext->storageName, false);
}
bool glass::SaveStorage(std::string_view dir) {
return SaveStorageImpl(gContext, dir, gContext->storageName, false);
}
Storage& glass::GetCurStorageRoot() {
return *gContext->storageStack.front();
}
Storage& glass::GetStorageRoot(std::string_view rootName) {
auto& storage = gContext->storageRoots[rootName];
if (!storage) {
storage = std::make_unique<Storage>();
}
return *storage;
}
void glass::ResetStorageStack(std::string_view rootName) {
if (gContext->storageStack.size() != 1) {
ImGui::LogText("resetting non-empty storage stack");
}
gContext->storageStack.clear();
gContext->storageStack.emplace_back(&GetStorageRoot(rootName));
}
Storage& glass::GetStorage() {
return *gContext->storageStack.back();
}
void glass::PushStorageStack(std::string_view label_id) {
gContext->storageStack.emplace_back(
&gContext->storageStack.back()->GetChild(label_id));
}
void glass::PushStorageStack(Storage& storage) {
gContext->storageStack.emplace_back(&storage);
}
void glass::PopStorageStack() {
if (gContext->storageStack.size() <= 1) {
ImGui::LogText("attempted to pop empty storage stack, mismatch push/pop?");
return; // ignore
}
gContext->storageStack.pop_back();
}
bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
PushStorageStack(name);
return ImGui::Begin(name, p_open, flags);
}
void glass::End() {
ImGui::End();
PopStorageStack();
}
bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
ImGuiWindowFlags flags) {
PushStorageStack(str_id);
return ImGui::BeginChild(str_id, size, border, flags);
}
void glass::EndChild() {
ImGui::EndChild();
PopStorageStack();
}
bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
bool& open = GetStorage().GetChild(label).GetBool(
"open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
ImGui::SetNextItemOpen(open);
open = ImGui::CollapsingHeader(label, flags);
return open;
}
bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
PushStorageStack(label);
bool& open = GetStorage().GetBool(
"open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
ImGui::SetNextItemOpen(open);
open = ImGui::TreeNodeEx(label, flags);
if (!open) {
PopStorageStack();
}
return open;
}
void glass::TreePop() {
ImGui::TreePop();
PopStorageStack();
}
void glass::PushID(const char* str_id) {
PushStorageStack(str_id);
ImGui::PushID(str_id);
}
void glass::PushID(const char* str_id_begin, const char* str_id_end) {
PushStorageStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
ImGui::PushID(str_id_begin, str_id_end);
}
void glass::PushID(int int_id) {
char buf[16];
std::snprintf(buf, sizeof(buf), "%d", int_id);
PushStorageStack(buf);
ImGui::PushID(int_id);
}
void glass::PopID() {
ImGui::PopID();
PopStorageStack();
}
bool glass::PopupEditName(const char* label, std::string* name) {
bool rv = false;
if (ImGui::BeginPopupContextItem(label)) {
rv = ItemEditName(name);
ImGui::EndPopup();
}
return rv;
}
bool glass::ItemEditName(std::string* name) {
bool rv = false;
ImGui::Text("Edit name:");
if (ImGui::InputText("##editname", name)) {
rv = true;
}
if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
ImGui::CloseCurrentPopup();
}
return rv;
}