blob: 5adcd943bb126276594f8845d4a4117b3bb3b782 [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>
Austin Schuh75263e32022-02-22 18:05:32 -08009#include <filesystem>
Austin Schuh812d0d12021-11-04 20:16:48 -070010
Austin Schuh75263e32022-02-22 18:05:32 -080011#include <fmt/format.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070012#include <imgui.h>
13#include <imgui_internal.h>
14#include <imgui_stdlib.h>
James Kuszmaulb13e13f2023-11-22 20:44:04 -080015#include <wpi/MemoryBuffer.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070016#include <wpi/StringExtras.h>
Austin Schuh75263e32022-02-22 18:05:32 -080017#include <wpi/fs.h>
18#include <wpi/json.h>
Austin Schuh75263e32022-02-22 18:05:32 -080019#include <wpi/raw_ostream.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070020#include <wpi/timestamp.h>
21#include <wpigui.h>
Austin Schuh75263e32022-02-22 18:05:32 -080022#include <wpigui_internal.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070023
24#include "glass/ContextInternal.h"
25
26using namespace glass;
27
28Context* glass::gContext;
29
Austin Schuh75263e32022-02-22 18:05:32 -080030static void WorkspaceResetImpl() {
31 // call reset functions
32 for (auto&& reset : gContext->workspaceReset) {
33 if (reset) {
34 reset();
35 }
Austin Schuh812d0d12021-11-04 20:16:48 -070036 }
Austin Schuh75263e32022-02-22 18:05:32 -080037
38 // clear storage
39 for (auto&& root : gContext->storageRoots) {
40 root.second->Clear();
41 }
42
43 // ImGui reset
44 ImGui::ClearIniSettings();
Austin Schuh812d0d12021-11-04 20:16:48 -070045}
46
Austin Schuh75263e32022-02-22 18:05:32 -080047static void WorkspaceInit() {
48 for (auto&& init : gContext->workspaceInit) {
49 if (init) {
50 init();
51 }
Austin Schuh812d0d12021-11-04 20:16:48 -070052 }
Austin Schuh75263e32022-02-22 18:05:32 -080053
54 for (auto&& root : gContext->storageRoots) {
55 root.getValue()->Apply();
56 }
Austin Schuh812d0d12021-11-04 20:16:48 -070057}
58
Austin Schuh75263e32022-02-22 18:05:32 -080059static bool JsonToWindow(const wpi::json& jfile, const char* filename) {
60 if (!jfile.is_object()) {
61 ImGui::LogText("%s top level is not object", filename);
62 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -070063 }
Austin Schuh75263e32022-02-22 18:05:32 -080064
65 // loop over JSON and generate ini format
66 std::string iniStr;
67 wpi::raw_string_ostream ini{iniStr};
68
69 for (auto&& jsection : jfile.items()) {
James Kuszmaulcf324122023-01-14 14:07:17 -080070 if (jsection.key() == "Docking") {
71 continue;
72 }
Austin Schuh75263e32022-02-22 18:05:32 -080073 if (!jsection.value().is_object()) {
74 ImGui::LogText("%s section %s is not object", filename,
75 jsection.key().c_str());
76 return false;
77 }
78 for (auto&& jsubsection : jsection.value().items()) {
79 if (!jsubsection.value().is_object()) {
80 ImGui::LogText("%s section %s subsection %s is not object", filename,
81 jsection.key().c_str(), jsubsection.key().c_str());
82 return false;
83 }
84 ini << '[' << jsection.key() << "][" << jsubsection.key() << "]\n";
85 for (auto&& jkv : jsubsection.value().items()) {
86 try {
87 auto& value = jkv.value().get_ref<const std::string&>();
88 ini << jkv.key() << '=' << value << "\n";
89 } catch (wpi::json::exception&) {
90 ImGui::LogText("%s section %s subsection %s value %s is not string",
91 filename, jsection.key().c_str(),
92 jsubsection.key().c_str(), jkv.key().c_str());
93 return false;
94 }
95 }
96 ini << '\n';
97 }
98 }
James Kuszmaulcf324122023-01-14 14:07:17 -080099
100 // emit Docking section last
101 auto docking = jfile.find("Docking");
102 if (docking != jfile.end()) {
103 for (auto&& jsubsection : docking->items()) {
104 if (!jsubsection.value().is_array()) {
105 ImGui::LogText("%s section %s subsection %s is not array", filename,
106 "Docking", jsubsection.key().c_str());
107 return false;
108 }
109 ini << "[Docking][" << jsubsection.key() << "]\n";
110 for (auto&& jv : jsubsection.value()) {
111 try {
112 auto& value = jv.get_ref<const std::string&>();
113 ini << value << "\n";
114 } catch (wpi::json::exception&) {
115 ImGui::LogText("%s section %s subsection %s value is not string",
116 filename, "Docking", jsubsection.key().c_str());
117 return false;
118 }
119 }
120 ini << '\n';
121 }
122 }
123
Austin Schuh75263e32022-02-22 18:05:32 -0800124 ini.flush();
125
126 ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
127 return true;
Austin Schuh812d0d12021-11-04 20:16:48 -0700128}
129
Austin Schuh75263e32022-02-22 18:05:32 -0800130static bool LoadWindowStorageImpl(const std::string& filename) {
131 std::error_code ec;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800132 std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
133 wpi::MemoryBuffer::GetFile(filename, ec);
134 if (fileBuffer == nullptr || ec) {
Austin Schuh75263e32022-02-22 18:05:32 -0800135 ImGui::LogText("error opening %s: %s", filename.c_str(),
136 ec.message().c_str());
137 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700138 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800139 try {
Maxwell Henderson80bec322024-01-09 15:48:44 -0800140 return JsonToWindow(wpi::json::parse(fileBuffer->GetCharBuffer()),
141 filename.c_str());
Austin Schuh75263e32022-02-22 18:05:32 -0800142 } 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;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800152 std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
153 wpi::MemoryBuffer::GetFile(filename, ec);
154 if (fileBuffer == nullptr || ec) {
Austin Schuh75263e32022-02-22 18:05:32 -0800155 ImGui::LogText("error opening %s: %s", filename.c_str(),
156 ec.message().c_str());
157 return false;
158 } else {
159 auto& storage = ctx->storageRoots[rootName];
160 bool createdStorage = false;
161 if (!storage) {
162 storage = std::make_unique<Storage>();
163 createdStorage = true;
164 }
165 try {
Maxwell Henderson80bec322024-01-09 15:48:44 -0800166 storage->FromJson(wpi::json::parse(fileBuffer->GetCharBuffer()),
167 filename.c_str());
Austin Schuh75263e32022-02-22 18:05:32 -0800168 } catch (wpi::json::parse_error& e) {
169 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
170 if (createdStorage) {
171 ctx->storageRoots.erase(rootName);
172 }
173 return false;
174 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700175 }
Austin Schuh75263e32022-02-22 18:05:32 -0800176 return true;
177}
Austin Schuh812d0d12021-11-04 20:16:48 -0700178
Austin Schuh75263e32022-02-22 18:05:32 -0800179static bool LoadStorageImpl(Context* ctx, std::string_view dir,
180 std::string_view name) {
Austin Schuh75263e32022-02-22 18:05:32 -0800181 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);
Maxwell Henderson80bec322024-01-09 15:48:44 -0800422 WorkspaceResetImpl();
Austin Schuh75263e32022-02-22 18:05:32 -0800423 LoadWindowStorageImpl((fs::path{gContext->storageLoadDir} /
424 fmt::format("{}-window.json", gContext->storageName))
425 .string());
426 return LoadStorageImpl(gContext, dir, gContext->storageName);
427}
428
429bool glass::SaveStorage() {
430 return SaveStorageImpl(gContext, gContext->storageAutoSaveDir,
431 gContext->storageName, false);
432}
433
434bool glass::SaveStorage(std::string_view dir) {
435 return SaveStorageImpl(gContext, dir, gContext->storageName, false);
436}
437
438Storage& glass::GetCurStorageRoot() {
439 return *gContext->storageStack.front();
440}
441
442Storage& glass::GetStorageRoot(std::string_view rootName) {
443 auto& storage = gContext->storageRoots[rootName];
444 if (!storage) {
445 storage = std::make_unique<Storage>();
446 }
447 return *storage;
448}
449
450void glass::ResetStorageStack(std::string_view rootName) {
451 if (gContext->storageStack.size() != 1) {
452 ImGui::LogText("resetting non-empty storage stack");
453 }
454 gContext->storageStack.clear();
455 gContext->storageStack.emplace_back(&GetStorageRoot(rootName));
456}
457
Austin Schuh812d0d12021-11-04 20:16:48 -0700458Storage& glass::GetStorage() {
Austin Schuh75263e32022-02-22 18:05:32 -0800459 return *gContext->storageStack.back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700460}
461
Austin Schuh75263e32022-02-22 18:05:32 -0800462void glass::PushStorageStack(std::string_view label_id) {
463 gContext->storageStack.emplace_back(
464 &gContext->storageStack.back()->GetChild(label_id));
Austin Schuh812d0d12021-11-04 20:16:48 -0700465}
466
Austin Schuh75263e32022-02-22 18:05:32 -0800467void glass::PushStorageStack(Storage& storage) {
468 gContext->storageStack.emplace_back(&storage);
Austin Schuh812d0d12021-11-04 20:16:48 -0700469}
470
Austin Schuh75263e32022-02-22 18:05:32 -0800471void glass::PopStorageStack() {
472 if (gContext->storageStack.size() <= 1) {
473 ImGui::LogText("attempted to pop empty storage stack, mismatch push/pop?");
474 return; // ignore
475 }
476 gContext->storageStack.pop_back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700477}
478
479bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800480 PushStorageStack(name);
Austin Schuh812d0d12021-11-04 20:16:48 -0700481 return ImGui::Begin(name, p_open, flags);
482}
483
484void glass::End() {
485 ImGui::End();
Austin Schuh75263e32022-02-22 18:05:32 -0800486 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700487}
488
489bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
490 ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800491 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700492 return ImGui::BeginChild(str_id, size, border, flags);
493}
494
495void glass::EndChild() {
496 ImGui::EndChild();
Austin Schuh75263e32022-02-22 18:05:32 -0800497 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700498}
499
500bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800501 bool& open = GetStorage().GetChild(label).GetBool(
502 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
503 ImGui::SetNextItemOpen(open);
504 open = ImGui::CollapsingHeader(label, flags);
505 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700506}
507
508bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800509 PushStorageStack(label);
510 bool& open = GetStorage().GetBool(
511 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
512 ImGui::SetNextItemOpen(open);
513 open = ImGui::TreeNodeEx(label, flags);
514 if (!open) {
515 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700516 }
Austin Schuh75263e32022-02-22 18:05:32 -0800517 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700518}
519
520void glass::TreePop() {
521 ImGui::TreePop();
Austin Schuh75263e32022-02-22 18:05:32 -0800522 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700523}
524
525void glass::PushID(const char* str_id) {
Austin Schuh75263e32022-02-22 18:05:32 -0800526 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700527 ImGui::PushID(str_id);
528}
529
530void glass::PushID(const char* str_id_begin, const char* str_id_end) {
Austin Schuh75263e32022-02-22 18:05:32 -0800531 PushStorageStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
Austin Schuh812d0d12021-11-04 20:16:48 -0700532 ImGui::PushID(str_id_begin, str_id_end);
533}
534
535void glass::PushID(int int_id) {
536 char buf[16];
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800537 wpi::format_to_n_c_str(buf, sizeof(buf), "{}", int_id);
538
Austin Schuh75263e32022-02-22 18:05:32 -0800539 PushStorageStack(buf);
Austin Schuh812d0d12021-11-04 20:16:48 -0700540 ImGui::PushID(int_id);
541}
542
543void glass::PopID() {
544 ImGui::PopID();
Austin Schuh75263e32022-02-22 18:05:32 -0800545 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700546}
547
548bool glass::PopupEditName(const char* label, std::string* name) {
549 bool rv = false;
550 if (ImGui::BeginPopupContextItem(label)) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800551 rv = ItemEditName(name);
552
Austin Schuh812d0d12021-11-04 20:16:48 -0700553 ImGui::EndPopup();
554 }
555 return rv;
556}
James Kuszmaulcf324122023-01-14 14:07:17 -0800557
558bool glass::ItemEditName(std::string* name) {
559 bool rv = false;
560
561 ImGui::Text("Edit name:");
562 if (ImGui::InputText("##editname", name)) {
563 rv = true;
564 }
565 if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
566 ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
567 ImGui::CloseCurrentPopup();
568 }
569
570 return rv;
571}