blob: 1de4af88400808ba6f0646ca59336afd3c96355f [file] [log] [blame]
Austin Schuh812d0d12021-11-04 20:16:48 -07001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#include "glass/Context.h"
6
7#include <algorithm>
8#include <cinttypes>
9#include <cstdio>
Austin Schuh75263e32022-02-22 18:05:32 -080010#include <filesystem>
Austin Schuh812d0d12021-11-04 20:16:48 -070011
Austin Schuh75263e32022-02-22 18:05:32 -080012#include <fmt/format.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070013#include <imgui.h>
14#include <imgui_internal.h>
15#include <imgui_stdlib.h>
16#include <wpi/StringExtras.h>
Austin Schuh75263e32022-02-22 18:05:32 -080017#include <wpi/fs.h>
18#include <wpi/json.h>
19#include <wpi/json_serializer.h>
20#include <wpi/raw_istream.h>
21#include <wpi/raw_ostream.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070022#include <wpi/timestamp.h>
23#include <wpigui.h>
Austin Schuh75263e32022-02-22 18:05:32 -080024#include <wpigui_internal.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070025
26#include "glass/ContextInternal.h"
27
28using namespace glass;
29
30Context* glass::gContext;
31
Austin Schuh75263e32022-02-22 18:05:32 -080032static void WorkspaceResetImpl() {
33 // call reset functions
34 for (auto&& reset : gContext->workspaceReset) {
35 if (reset) {
36 reset();
37 }
Austin Schuh812d0d12021-11-04 20:16:48 -070038 }
Austin Schuh75263e32022-02-22 18:05:32 -080039
40 // clear storage
41 for (auto&& root : gContext->storageRoots) {
42 root.second->Clear();
43 }
44
45 // ImGui reset
46 ImGui::ClearIniSettings();
Austin Schuh812d0d12021-11-04 20:16:48 -070047}
48
Austin Schuh75263e32022-02-22 18:05:32 -080049static void WorkspaceInit() {
50 for (auto&& init : gContext->workspaceInit) {
51 if (init) {
52 init();
53 }
Austin Schuh812d0d12021-11-04 20:16:48 -070054 }
Austin Schuh75263e32022-02-22 18:05:32 -080055
56 for (auto&& root : gContext->storageRoots) {
57 root.getValue()->Apply();
58 }
Austin Schuh812d0d12021-11-04 20:16:48 -070059}
60
Austin Schuh75263e32022-02-22 18:05:32 -080061static bool JsonToWindow(const wpi::json& jfile, const char* filename) {
62 if (!jfile.is_object()) {
63 ImGui::LogText("%s top level is not object", filename);
64 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -070065 }
Austin Schuh75263e32022-02-22 18:05:32 -080066
67 // loop over JSON and generate ini format
68 std::string iniStr;
69 wpi::raw_string_ostream ini{iniStr};
70
71 for (auto&& jsection : jfile.items()) {
72 if (!jsection.value().is_object()) {
73 ImGui::LogText("%s section %s is not object", filename,
74 jsection.key().c_str());
75 return false;
76 }
77 for (auto&& jsubsection : jsection.value().items()) {
78 if (!jsubsection.value().is_object()) {
79 ImGui::LogText("%s section %s subsection %s is not object", filename,
80 jsection.key().c_str(), jsubsection.key().c_str());
81 return false;
82 }
83 ini << '[' << jsection.key() << "][" << jsubsection.key() << "]\n";
84 for (auto&& jkv : jsubsection.value().items()) {
85 try {
86 auto& value = jkv.value().get_ref<const std::string&>();
87 ini << jkv.key() << '=' << value << "\n";
88 } catch (wpi::json::exception&) {
89 ImGui::LogText("%s section %s subsection %s value %s is not string",
90 filename, jsection.key().c_str(),
91 jsubsection.key().c_str(), jkv.key().c_str());
92 return false;
93 }
94 }
95 ini << '\n';
96 }
97 }
98 ini.flush();
99
100 ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
101 return true;
Austin Schuh812d0d12021-11-04 20:16:48 -0700102}
103
Austin Schuh75263e32022-02-22 18:05:32 -0800104static bool LoadWindowStorageImpl(const std::string& filename) {
105 std::error_code ec;
106 wpi::raw_fd_istream is{filename, ec};
107 if (ec) {
108 ImGui::LogText("error opening %s: %s", filename.c_str(),
109 ec.message().c_str());
110 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700111 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800112 try {
113 return JsonToWindow(wpi::json::parse(is), filename.c_str());
114 } catch (wpi::json::parse_error& e) {
115 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
116 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700117 }
118 }
119}
120
Austin Schuh75263e32022-02-22 18:05:32 -0800121static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
122 std::string_view rootName) {
123 std::error_code ec;
124 wpi::raw_fd_istream is{filename, ec};
125 if (ec) {
126 ImGui::LogText("error opening %s: %s", filename.c_str(),
127 ec.message().c_str());
128 return false;
129 } else {
130 auto& storage = ctx->storageRoots[rootName];
131 bool createdStorage = false;
132 if (!storage) {
133 storage = std::make_unique<Storage>();
134 createdStorage = true;
135 }
136 try {
137 storage->FromJson(wpi::json::parse(is), filename.c_str());
138 } catch (wpi::json::parse_error& e) {
139 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
140 if (createdStorage) {
141 ctx->storageRoots.erase(rootName);
142 }
143 return false;
144 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700145 }
Austin Schuh75263e32022-02-22 18:05:32 -0800146 return true;
147}
Austin Schuh812d0d12021-11-04 20:16:48 -0700148
Austin Schuh75263e32022-02-22 18:05:32 -0800149static bool LoadStorageImpl(Context* ctx, std::string_view dir,
150 std::string_view name) {
151 WorkspaceResetImpl();
152
153 bool rv = true;
154 for (auto&& root : ctx->storageRoots) {
155 std::string filename;
156 auto rootName = root.getKey();
157 if (rootName.empty()) {
158 filename = (fs::path{dir} / fmt::format("{}.json", name)).string();
159 } else {
160 filename =
161 (fs::path{dir} / fmt::format("{}-{}.json", name, rootName)).string();
162 }
163 if (!LoadStorageRootImpl(ctx, filename, rootName)) {
164 rv = false;
165 }
166 }
167
168 WorkspaceInit();
169 return rv;
170}
171
172static wpi::json WindowToJson() {
173 size_t iniLen;
174 const char* iniData = ImGui::SaveIniSettingsToMemory(&iniLen);
175 std::string_view ini{iniData, iniLen};
176
177 // parse the ini data and build JSON
178 // JSON format:
179 // {
180 // "Section": {
181 // "Subsection": {
182 // "Key": "Value" // all values are saved as strings
183 // }
184 // }
185 // }
186
187 wpi::json out = wpi::json::object();
188 wpi::json* curSection = nullptr;
189 while (!ini.empty()) {
190 std::string_view line;
191 std::tie(line, ini) = wpi::split(ini, '\n');
192 line = wpi::trim(line);
193 if (line.empty()) {
194 continue;
195 }
196 if (line[0] == '[') {
197 // new section
198 auto [section, subsection] = wpi::split(line, ']');
199 section = wpi::drop_front(section); // drop '['; ']' was dropped by split
200 subsection = wpi::drop_back(wpi::drop_front(subsection)); // drop []
201 auto& jsection = out[section];
202 if (jsection.is_null()) {
203 jsection = wpi::json::object();
204 }
205 curSection = &jsection[subsection];
206 if (curSection->is_null()) {
207 *curSection = wpi::json::object();
208 }
209 } else {
210 // value
211 if (!curSection) {
212 continue; // shouldn't happen, but just in case
213 }
214 auto [name, value] = wpi::split(line, '=');
215 (*curSection)[name] = value;
216 }
217 }
218
219 return out;
220}
221
222bool SaveWindowStorageImpl(const std::string& filename) {
223 std::error_code ec;
224 wpi::raw_fd_ostream os{filename, ec};
225 if (ec) {
226 ImGui::LogText("error opening %s: %s", filename.c_str(),
227 ec.message().c_str());
228 return false;
229 }
230 WindowToJson().dump(os, 2);
231 os << '\n';
232 return true;
233}
234
235static bool SaveStorageRootImpl(Context* ctx, const std::string& filename,
236 const Storage& storage) {
237 std::error_code ec;
238 wpi::raw_fd_ostream os{filename, ec};
239 if (ec) {
240 ImGui::LogText("error opening %s: %s", filename.c_str(),
241 ec.message().c_str());
242 return false;
243 }
244 storage.ToJson().dump(os, 2);
245 os << '\n';
246 return true;
247}
248
249static bool SaveStorageImpl(Context* ctx, std::string_view dir,
250 std::string_view name, bool exiting) {
251 fs::path dirPath{dir};
252
253 std::error_code ec;
254 fs::create_directories(dirPath, ec);
255 if (ec) {
256 return false;
257 }
258
259 // handle erasing save files on exit if requested
260 if (exiting && wpi::gui::gContext->resetOnExit) {
261 fs::remove(dirPath / fmt::format("{}-window.json", name), ec);
262 for (auto&& root : ctx->storageRoots) {
263 auto rootName = root.getKey();
264 if (rootName.empty()) {
265 fs::remove(dirPath / fmt::format("{}.json", name), ec);
266 } else {
267 fs::remove(dirPath / fmt::format("{}-{}.json", name, rootName), ec);
Austin Schuh812d0d12021-11-04 20:16:48 -0700268 }
269 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700270 }
Austin Schuh75263e32022-02-22 18:05:32 -0800271
272 bool rv = SaveWindowStorageImpl(
273 (dirPath / fmt::format("{}-window.json", name)).string());
274
275 for (auto&& root : ctx->storageRoots) {
276 auto rootName = root.getKey();
277 std::string filename;
278 if (rootName.empty()) {
279 filename = (dirPath / fmt::format("{}.json", name)).string();
280 } else {
281 filename = (dirPath / fmt::format("{}-{}.json", name, rootName)).string();
282 }
283 if (!SaveStorageRootImpl(ctx, filename, *root.getValue())) {
284 rv = false;
285 }
286 }
287 return rv;
Austin Schuh812d0d12021-11-04 20:16:48 -0700288}
289
Austin Schuh75263e32022-02-22 18:05:32 -0800290Context::Context()
291 : sourceNameStorage{storageRoots.insert({"", std::make_unique<Storage>()})
292 .first->getValue()
293 ->GetChild("sourceNames")} {
294 storageStack.emplace_back(storageRoots[""].get());
Austin Schuh812d0d12021-11-04 20:16:48 -0700295
Austin Schuh75263e32022-02-22 18:05:32 -0800296 // override ImGui ini saving
297 wpi::gui::ConfigureCustomSaveSettings(
298 [this] { LoadStorageImpl(this, storageLoadDir, storageName); },
299 [this] {
300 LoadWindowStorageImpl((fs::path{storageLoadDir} /
301 fmt::format("{}-window.json", storageName))
302 .string());
303 },
304 [this](bool exiting) {
305 SaveStorageImpl(this, storageAutoSaveDir, storageName, exiting);
306 });
Austin Schuh812d0d12021-11-04 20:16:48 -0700307}
308
Austin Schuh75263e32022-02-22 18:05:32 -0800309Context::~Context() {
310 wpi::gui::ConfigureCustomSaveSettings(nullptr, nullptr, nullptr);
311}
Austin Schuh812d0d12021-11-04 20:16:48 -0700312
313Context* glass::CreateContext() {
314 Context* ctx = new Context;
315 if (!gContext) {
316 SetCurrentContext(ctx);
317 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700318 return ctx;
319}
320
321void glass::DestroyContext(Context* ctx) {
322 if (!ctx) {
323 ctx = gContext;
324 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700325 if (gContext == ctx) {
326 SetCurrentContext(nullptr);
327 }
328 delete ctx;
329}
330
331Context* glass::GetCurrentContext() {
332 return gContext;
333}
334
335void glass::SetCurrentContext(Context* ctx) {
336 gContext = ctx;
337}
338
339void glass::ResetTime() {
340 gContext->zeroTime = wpi::Now();
341}
342
343uint64_t glass::GetZeroTime() {
344 return gContext->zeroTime;
345}
346
Austin Schuh75263e32022-02-22 18:05:32 -0800347void glass::WorkspaceReset() {
348 WorkspaceResetImpl();
349 WorkspaceInit();
350}
351
352void glass::AddWorkspaceInit(std::function<void()> init) {
353 if (init) {
354 gContext->workspaceInit.emplace_back(std::move(init));
Austin Schuh812d0d12021-11-04 20:16:48 -0700355 }
356}
357
Austin Schuh75263e32022-02-22 18:05:32 -0800358void glass::AddWorkspaceReset(std::function<void()> reset) {
359 if (reset) {
360 gContext->workspaceReset.emplace_back(std::move(reset));
Austin Schuh812d0d12021-11-04 20:16:48 -0700361 }
362}
363
Austin Schuh75263e32022-02-22 18:05:32 -0800364void glass::SetStorageName(std::string_view name) {
365 gContext->storageName = name;
366}
367
368void glass::SetStorageDir(std::string_view dir) {
369 if (dir.empty()) {
370 gContext->storageLoadDir = ".";
371 gContext->storageAutoSaveDir = ".";
Austin Schuh812d0d12021-11-04 20:16:48 -0700372 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800373 gContext->storageLoadDir = dir;
374 gContext->storageAutoSaveDir = dir;
375 gContext->isPlatformSaveDir = (dir == wpi::gui::GetPlatformSaveFileDir());
Austin Schuh812d0d12021-11-04 20:16:48 -0700376 }
377}
378
Austin Schuh75263e32022-02-22 18:05:32 -0800379std::string glass::GetStorageDir() {
380 return gContext->storageAutoSaveDir;
381}
382
383bool glass::LoadStorage(std::string_view dir) {
384 SaveStorage();
385 SetStorageDir(dir);
386 LoadWindowStorageImpl((fs::path{gContext->storageLoadDir} /
387 fmt::format("{}-window.json", gContext->storageName))
388 .string());
389 return LoadStorageImpl(gContext, dir, gContext->storageName);
390}
391
392bool glass::SaveStorage() {
393 return SaveStorageImpl(gContext, gContext->storageAutoSaveDir,
394 gContext->storageName, false);
395}
396
397bool glass::SaveStorage(std::string_view dir) {
398 return SaveStorageImpl(gContext, dir, gContext->storageName, false);
399}
400
401Storage& glass::GetCurStorageRoot() {
402 return *gContext->storageStack.front();
403}
404
405Storage& glass::GetStorageRoot(std::string_view rootName) {
406 auto& storage = gContext->storageRoots[rootName];
407 if (!storage) {
408 storage = std::make_unique<Storage>();
409 }
410 return *storage;
411}
412
413void glass::ResetStorageStack(std::string_view rootName) {
414 if (gContext->storageStack.size() != 1) {
415 ImGui::LogText("resetting non-empty storage stack");
416 }
417 gContext->storageStack.clear();
418 gContext->storageStack.emplace_back(&GetStorageRoot(rootName));
419}
420
Austin Schuh812d0d12021-11-04 20:16:48 -0700421Storage& glass::GetStorage() {
Austin Schuh75263e32022-02-22 18:05:32 -0800422 return *gContext->storageStack.back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700423}
424
Austin Schuh75263e32022-02-22 18:05:32 -0800425void glass::PushStorageStack(std::string_view label_id) {
426 gContext->storageStack.emplace_back(
427 &gContext->storageStack.back()->GetChild(label_id));
Austin Schuh812d0d12021-11-04 20:16:48 -0700428}
429
Austin Schuh75263e32022-02-22 18:05:32 -0800430void glass::PushStorageStack(Storage& storage) {
431 gContext->storageStack.emplace_back(&storage);
Austin Schuh812d0d12021-11-04 20:16:48 -0700432}
433
Austin Schuh75263e32022-02-22 18:05:32 -0800434void glass::PopStorageStack() {
435 if (gContext->storageStack.size() <= 1) {
436 ImGui::LogText("attempted to pop empty storage stack, mismatch push/pop?");
437 return; // ignore
438 }
439 gContext->storageStack.pop_back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700440}
441
442bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800443 PushStorageStack(name);
Austin Schuh812d0d12021-11-04 20:16:48 -0700444 return ImGui::Begin(name, p_open, flags);
445}
446
447void glass::End() {
448 ImGui::End();
Austin Schuh75263e32022-02-22 18:05:32 -0800449 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700450}
451
452bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
453 ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800454 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700455 return ImGui::BeginChild(str_id, size, border, flags);
456}
457
458void glass::EndChild() {
459 ImGui::EndChild();
Austin Schuh75263e32022-02-22 18:05:32 -0800460 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700461}
462
463bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800464 bool& open = GetStorage().GetChild(label).GetBool(
465 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
466 ImGui::SetNextItemOpen(open);
467 open = ImGui::CollapsingHeader(label, flags);
468 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700469}
470
471bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800472 PushStorageStack(label);
473 bool& open = GetStorage().GetBool(
474 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
475 ImGui::SetNextItemOpen(open);
476 open = ImGui::TreeNodeEx(label, flags);
477 if (!open) {
478 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700479 }
Austin Schuh75263e32022-02-22 18:05:32 -0800480 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700481}
482
483void glass::TreePop() {
484 ImGui::TreePop();
Austin Schuh75263e32022-02-22 18:05:32 -0800485 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700486}
487
488void glass::PushID(const char* str_id) {
Austin Schuh75263e32022-02-22 18:05:32 -0800489 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700490 ImGui::PushID(str_id);
491}
492
493void glass::PushID(const char* str_id_begin, const char* str_id_end) {
Austin Schuh75263e32022-02-22 18:05:32 -0800494 PushStorageStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
Austin Schuh812d0d12021-11-04 20:16:48 -0700495 ImGui::PushID(str_id_begin, str_id_end);
496}
497
498void glass::PushID(int int_id) {
499 char buf[16];
500 std::snprintf(buf, sizeof(buf), "%d", int_id);
Austin Schuh75263e32022-02-22 18:05:32 -0800501 PushStorageStack(buf);
Austin Schuh812d0d12021-11-04 20:16:48 -0700502 ImGui::PushID(int_id);
503}
504
505void glass::PopID() {
506 ImGui::PopID();
Austin Schuh75263e32022-02-22 18:05:32 -0800507 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700508}
509
510bool glass::PopupEditName(const char* label, std::string* name) {
511 bool rv = false;
512 if (ImGui::BeginPopupContextItem(label)) {
513 ImGui::Text("Edit name:");
514 if (ImGui::InputText("##editname", name)) {
515 rv = true;
516 }
517 if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
518 ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
519 ImGui::CloseCurrentPopup();
520 }
521 ImGui::EndPopup();
522 }
523 return rv;
524}