blob: a55cf8273dfc570f66a1f4fedfbf64c4049efb6d [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()) {
James Kuszmaulcf324122023-01-14 14:07:17 -080072 if (jsection.key() == "Docking") {
73 continue;
74 }
Austin Schuh75263e32022-02-22 18:05:32 -080075 if (!jsection.value().is_object()) {
76 ImGui::LogText("%s section %s is not object", filename,
77 jsection.key().c_str());
78 return false;
79 }
80 for (auto&& jsubsection : jsection.value().items()) {
81 if (!jsubsection.value().is_object()) {
82 ImGui::LogText("%s section %s subsection %s is not object", filename,
83 jsection.key().c_str(), jsubsection.key().c_str());
84 return false;
85 }
86 ini << '[' << jsection.key() << "][" << jsubsection.key() << "]\n";
87 for (auto&& jkv : jsubsection.value().items()) {
88 try {
89 auto& value = jkv.value().get_ref<const std::string&>();
90 ini << jkv.key() << '=' << value << "\n";
91 } catch (wpi::json::exception&) {
92 ImGui::LogText("%s section %s subsection %s value %s is not string",
93 filename, jsection.key().c_str(),
94 jsubsection.key().c_str(), jkv.key().c_str());
95 return false;
96 }
97 }
98 ini << '\n';
99 }
100 }
James Kuszmaulcf324122023-01-14 14:07:17 -0800101
102 // emit Docking section last
103 auto docking = jfile.find("Docking");
104 if (docking != jfile.end()) {
105 for (auto&& jsubsection : docking->items()) {
106 if (!jsubsection.value().is_array()) {
107 ImGui::LogText("%s section %s subsection %s is not array", filename,
108 "Docking", jsubsection.key().c_str());
109 return false;
110 }
111 ini << "[Docking][" << jsubsection.key() << "]\n";
112 for (auto&& jv : jsubsection.value()) {
113 try {
114 auto& value = jv.get_ref<const std::string&>();
115 ini << value << "\n";
116 } catch (wpi::json::exception&) {
117 ImGui::LogText("%s section %s subsection %s value is not string",
118 filename, "Docking", jsubsection.key().c_str());
119 return false;
120 }
121 }
122 ini << '\n';
123 }
124 }
125
Austin Schuh75263e32022-02-22 18:05:32 -0800126 ini.flush();
127
128 ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
129 return true;
Austin Schuh812d0d12021-11-04 20:16:48 -0700130}
131
Austin Schuh75263e32022-02-22 18:05:32 -0800132static bool LoadWindowStorageImpl(const std::string& filename) {
133 std::error_code ec;
134 wpi::raw_fd_istream is{filename, ec};
135 if (ec) {
136 ImGui::LogText("error opening %s: %s", filename.c_str(),
137 ec.message().c_str());
138 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700139 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800140 try {
141 return JsonToWindow(wpi::json::parse(is), filename.c_str());
142 } catch (wpi::json::parse_error& e) {
143 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
144 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700145 }
146 }
147}
148
Austin Schuh75263e32022-02-22 18:05:32 -0800149static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
150 std::string_view rootName) {
151 std::error_code ec;
152 wpi::raw_fd_istream is{filename, ec};
153 if (ec) {
154 ImGui::LogText("error opening %s: %s", filename.c_str(),
155 ec.message().c_str());
156 return false;
157 } else {
158 auto& storage = ctx->storageRoots[rootName];
159 bool createdStorage = false;
160 if (!storage) {
161 storage = std::make_unique<Storage>();
162 createdStorage = true;
163 }
164 try {
165 storage->FromJson(wpi::json::parse(is), filename.c_str());
166 } catch (wpi::json::parse_error& e) {
167 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
168 if (createdStorage) {
169 ctx->storageRoots.erase(rootName);
170 }
171 return false;
172 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700173 }
Austin Schuh75263e32022-02-22 18:05:32 -0800174 return true;
175}
Austin Schuh812d0d12021-11-04 20:16:48 -0700176
Austin Schuh75263e32022-02-22 18:05:32 -0800177static bool LoadStorageImpl(Context* ctx, std::string_view dir,
178 std::string_view name) {
179 WorkspaceResetImpl();
180
181 bool rv = true;
182 for (auto&& root : ctx->storageRoots) {
183 std::string filename;
184 auto rootName = root.getKey();
185 if (rootName.empty()) {
186 filename = (fs::path{dir} / fmt::format("{}.json", name)).string();
187 } else {
188 filename =
189 (fs::path{dir} / fmt::format("{}-{}.json", name, rootName)).string();
190 }
191 if (!LoadStorageRootImpl(ctx, filename, rootName)) {
192 rv = false;
193 }
194 }
195
196 WorkspaceInit();
197 return rv;
198}
199
200static wpi::json WindowToJson() {
201 size_t iniLen;
202 const char* iniData = ImGui::SaveIniSettingsToMemory(&iniLen);
203 std::string_view ini{iniData, iniLen};
204
205 // parse the ini data and build JSON
206 // JSON format:
207 // {
208 // "Section": {
209 // "Subsection": {
210 // "Key": "Value" // all values are saved as strings
211 // }
212 // }
213 // }
214
215 wpi::json out = wpi::json::object();
216 wpi::json* curSection = nullptr;
217 while (!ini.empty()) {
218 std::string_view line;
219 std::tie(line, ini) = wpi::split(ini, '\n');
220 line = wpi::trim(line);
221 if (line.empty()) {
222 continue;
223 }
224 if (line[0] == '[') {
225 // new section
226 auto [section, subsection] = wpi::split(line, ']');
227 section = wpi::drop_front(section); // drop '['; ']' was dropped by split
228 subsection = wpi::drop_back(wpi::drop_front(subsection)); // drop []
229 auto& jsection = out[section];
230 if (jsection.is_null()) {
231 jsection = wpi::json::object();
232 }
233 curSection = &jsection[subsection];
234 if (curSection->is_null()) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800235 if (section == "Docking") {
236 *curSection = wpi::json::array();
237 } else {
238 *curSection = wpi::json::object();
239 }
Austin Schuh75263e32022-02-22 18:05:32 -0800240 }
241 } else {
242 // value
243 if (!curSection) {
244 continue; // shouldn't happen, but just in case
245 }
246 auto [name, value] = wpi::split(line, '=');
James Kuszmaulcf324122023-01-14 14:07:17 -0800247 if (curSection->is_object()) {
248 (*curSection)[name] = value;
249 } else if (curSection->is_array()) {
250 curSection->emplace_back(line);
251 }
Austin Schuh75263e32022-02-22 18:05:32 -0800252 }
253 }
254
255 return out;
256}
257
258bool SaveWindowStorageImpl(const std::string& filename) {
259 std::error_code ec;
260 wpi::raw_fd_ostream os{filename, ec};
261 if (ec) {
262 ImGui::LogText("error opening %s: %s", filename.c_str(),
263 ec.message().c_str());
264 return false;
265 }
266 WindowToJson().dump(os, 2);
267 os << '\n';
268 return true;
269}
270
271static bool SaveStorageRootImpl(Context* ctx, const std::string& filename,
272 const Storage& storage) {
273 std::error_code ec;
274 wpi::raw_fd_ostream os{filename, ec};
275 if (ec) {
276 ImGui::LogText("error opening %s: %s", filename.c_str(),
277 ec.message().c_str());
278 return false;
279 }
280 storage.ToJson().dump(os, 2);
281 os << '\n';
282 return true;
283}
284
285static bool SaveStorageImpl(Context* ctx, std::string_view dir,
286 std::string_view name, bool exiting) {
287 fs::path dirPath{dir};
288
289 std::error_code ec;
290 fs::create_directories(dirPath, ec);
291 if (ec) {
292 return false;
293 }
294
295 // handle erasing save files on exit if requested
296 if (exiting && wpi::gui::gContext->resetOnExit) {
297 fs::remove(dirPath / fmt::format("{}-window.json", name), ec);
298 for (auto&& root : ctx->storageRoots) {
299 auto rootName = root.getKey();
300 if (rootName.empty()) {
301 fs::remove(dirPath / fmt::format("{}.json", name), ec);
302 } else {
303 fs::remove(dirPath / fmt::format("{}-{}.json", name, rootName), ec);
Austin Schuh812d0d12021-11-04 20:16:48 -0700304 }
305 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700306 }
Austin Schuh75263e32022-02-22 18:05:32 -0800307
308 bool rv = SaveWindowStorageImpl(
309 (dirPath / fmt::format("{}-window.json", name)).string());
310
311 for (auto&& root : ctx->storageRoots) {
312 auto rootName = root.getKey();
313 std::string filename;
314 if (rootName.empty()) {
315 filename = (dirPath / fmt::format("{}.json", name)).string();
316 } else {
317 filename = (dirPath / fmt::format("{}-{}.json", name, rootName)).string();
318 }
319 if (!SaveStorageRootImpl(ctx, filename, *root.getValue())) {
320 rv = false;
321 }
322 }
323 return rv;
Austin Schuh812d0d12021-11-04 20:16:48 -0700324}
325
Austin Schuh75263e32022-02-22 18:05:32 -0800326Context::Context()
327 : sourceNameStorage{storageRoots.insert({"", std::make_unique<Storage>()})
328 .first->getValue()
329 ->GetChild("sourceNames")} {
330 storageStack.emplace_back(storageRoots[""].get());
Austin Schuh812d0d12021-11-04 20:16:48 -0700331
Austin Schuh75263e32022-02-22 18:05:32 -0800332 // override ImGui ini saving
333 wpi::gui::ConfigureCustomSaveSettings(
334 [this] { LoadStorageImpl(this, storageLoadDir, storageName); },
335 [this] {
336 LoadWindowStorageImpl((fs::path{storageLoadDir} /
337 fmt::format("{}-window.json", storageName))
338 .string());
339 },
340 [this](bool exiting) {
341 SaveStorageImpl(this, storageAutoSaveDir, storageName, exiting);
342 });
Austin Schuh812d0d12021-11-04 20:16:48 -0700343}
344
Austin Schuh75263e32022-02-22 18:05:32 -0800345Context::~Context() {
346 wpi::gui::ConfigureCustomSaveSettings(nullptr, nullptr, nullptr);
347}
Austin Schuh812d0d12021-11-04 20:16:48 -0700348
349Context* glass::CreateContext() {
350 Context* ctx = new Context;
351 if (!gContext) {
352 SetCurrentContext(ctx);
353 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700354 return ctx;
355}
356
357void glass::DestroyContext(Context* ctx) {
358 if (!ctx) {
359 ctx = gContext;
360 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700361 if (gContext == ctx) {
362 SetCurrentContext(nullptr);
363 }
364 delete ctx;
365}
366
367Context* glass::GetCurrentContext() {
368 return gContext;
369}
370
371void glass::SetCurrentContext(Context* ctx) {
372 gContext = ctx;
373}
374
375void glass::ResetTime() {
376 gContext->zeroTime = wpi::Now();
377}
378
379uint64_t glass::GetZeroTime() {
380 return gContext->zeroTime;
381}
382
Austin Schuh75263e32022-02-22 18:05:32 -0800383void glass::WorkspaceReset() {
384 WorkspaceResetImpl();
385 WorkspaceInit();
386}
387
388void glass::AddWorkspaceInit(std::function<void()> init) {
389 if (init) {
390 gContext->workspaceInit.emplace_back(std::move(init));
Austin Schuh812d0d12021-11-04 20:16:48 -0700391 }
392}
393
Austin Schuh75263e32022-02-22 18:05:32 -0800394void glass::AddWorkspaceReset(std::function<void()> reset) {
395 if (reset) {
396 gContext->workspaceReset.emplace_back(std::move(reset));
Austin Schuh812d0d12021-11-04 20:16:48 -0700397 }
398}
399
Austin Schuh75263e32022-02-22 18:05:32 -0800400void glass::SetStorageName(std::string_view name) {
401 gContext->storageName = name;
402}
403
404void glass::SetStorageDir(std::string_view dir) {
405 if (dir.empty()) {
406 gContext->storageLoadDir = ".";
407 gContext->storageAutoSaveDir = ".";
Austin Schuh812d0d12021-11-04 20:16:48 -0700408 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800409 gContext->storageLoadDir = dir;
410 gContext->storageAutoSaveDir = dir;
411 gContext->isPlatformSaveDir = (dir == wpi::gui::GetPlatformSaveFileDir());
Austin Schuh812d0d12021-11-04 20:16:48 -0700412 }
413}
414
Austin Schuh75263e32022-02-22 18:05:32 -0800415std::string glass::GetStorageDir() {
416 return gContext->storageAutoSaveDir;
417}
418
419bool glass::LoadStorage(std::string_view dir) {
420 SaveStorage();
421 SetStorageDir(dir);
422 LoadWindowStorageImpl((fs::path{gContext->storageLoadDir} /
423 fmt::format("{}-window.json", gContext->storageName))
424 .string());
425 return LoadStorageImpl(gContext, dir, gContext->storageName);
426}
427
428bool glass::SaveStorage() {
429 return SaveStorageImpl(gContext, gContext->storageAutoSaveDir,
430 gContext->storageName, false);
431}
432
433bool glass::SaveStorage(std::string_view dir) {
434 return SaveStorageImpl(gContext, dir, gContext->storageName, false);
435}
436
437Storage& glass::GetCurStorageRoot() {
438 return *gContext->storageStack.front();
439}
440
441Storage& glass::GetStorageRoot(std::string_view rootName) {
442 auto& storage = gContext->storageRoots[rootName];
443 if (!storage) {
444 storage = std::make_unique<Storage>();
445 }
446 return *storage;
447}
448
449void glass::ResetStorageStack(std::string_view rootName) {
450 if (gContext->storageStack.size() != 1) {
451 ImGui::LogText("resetting non-empty storage stack");
452 }
453 gContext->storageStack.clear();
454 gContext->storageStack.emplace_back(&GetStorageRoot(rootName));
455}
456
Austin Schuh812d0d12021-11-04 20:16:48 -0700457Storage& glass::GetStorage() {
Austin Schuh75263e32022-02-22 18:05:32 -0800458 return *gContext->storageStack.back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700459}
460
Austin Schuh75263e32022-02-22 18:05:32 -0800461void glass::PushStorageStack(std::string_view label_id) {
462 gContext->storageStack.emplace_back(
463 &gContext->storageStack.back()->GetChild(label_id));
Austin Schuh812d0d12021-11-04 20:16:48 -0700464}
465
Austin Schuh75263e32022-02-22 18:05:32 -0800466void glass::PushStorageStack(Storage& storage) {
467 gContext->storageStack.emplace_back(&storage);
Austin Schuh812d0d12021-11-04 20:16:48 -0700468}
469
Austin Schuh75263e32022-02-22 18:05:32 -0800470void glass::PopStorageStack() {
471 if (gContext->storageStack.size() <= 1) {
472 ImGui::LogText("attempted to pop empty storage stack, mismatch push/pop?");
473 return; // ignore
474 }
475 gContext->storageStack.pop_back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700476}
477
478bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800479 PushStorageStack(name);
Austin Schuh812d0d12021-11-04 20:16:48 -0700480 return ImGui::Begin(name, p_open, flags);
481}
482
483void glass::End() {
484 ImGui::End();
Austin Schuh75263e32022-02-22 18:05:32 -0800485 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700486}
487
488bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
489 ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800490 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700491 return ImGui::BeginChild(str_id, size, border, flags);
492}
493
494void glass::EndChild() {
495 ImGui::EndChild();
Austin Schuh75263e32022-02-22 18:05:32 -0800496 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700497}
498
499bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800500 bool& open = GetStorage().GetChild(label).GetBool(
501 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
502 ImGui::SetNextItemOpen(open);
503 open = ImGui::CollapsingHeader(label, flags);
504 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700505}
506
507bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800508 PushStorageStack(label);
509 bool& open = GetStorage().GetBool(
510 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
511 ImGui::SetNextItemOpen(open);
512 open = ImGui::TreeNodeEx(label, flags);
513 if (!open) {
514 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700515 }
Austin Schuh75263e32022-02-22 18:05:32 -0800516 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700517}
518
519void glass::TreePop() {
520 ImGui::TreePop();
Austin Schuh75263e32022-02-22 18:05:32 -0800521 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700522}
523
524void glass::PushID(const char* str_id) {
Austin Schuh75263e32022-02-22 18:05:32 -0800525 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700526 ImGui::PushID(str_id);
527}
528
529void glass::PushID(const char* str_id_begin, const char* str_id_end) {
Austin Schuh75263e32022-02-22 18:05:32 -0800530 PushStorageStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
Austin Schuh812d0d12021-11-04 20:16:48 -0700531 ImGui::PushID(str_id_begin, str_id_end);
532}
533
534void glass::PushID(int int_id) {
535 char buf[16];
536 std::snprintf(buf, sizeof(buf), "%d", int_id);
Austin Schuh75263e32022-02-22 18:05:32 -0800537 PushStorageStack(buf);
Austin Schuh812d0d12021-11-04 20:16:48 -0700538 ImGui::PushID(int_id);
539}
540
541void glass::PopID() {
542 ImGui::PopID();
Austin Schuh75263e32022-02-22 18:05:32 -0800543 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700544}
545
546bool glass::PopupEditName(const char* label, std::string* name) {
547 bool rv = false;
548 if (ImGui::BeginPopupContextItem(label)) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800549 rv = ItemEditName(name);
550
Austin Schuh812d0d12021-11-04 20:16:48 -0700551 ImGui::EndPopup();
552 }
553 return rv;
554}
James Kuszmaulcf324122023-01-14 14:07:17 -0800555
556bool glass::ItemEditName(std::string* name) {
557 bool rv = false;
558
559 ImGui::Text("Edit name:");
560 if (ImGui::InputText("##editname", name)) {
561 rv = true;
562 }
563 if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
564 ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
565 ImGui::CloseCurrentPopup();
566 }
567
568 return rv;
569}