INSTINCT Code Coverage Report


Directory: src/
File: internal/gui/NodeEditorApplication.cpp
Date: 2025-06-06 13:17:47
Exec Total Coverage
Lines: 0 1031 0.0%
Functions: 0 19 0.0%
Branches: 0 2448 0.0%

Line Branch Exec Source
1 // This file is part of INSTINCT, the INS Toolkit for Integrated
2 // Navigation Concepts and Training by the Institute of Navigation of
3 // the University of Stuttgart, Germany.
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
9 #include "NodeEditorApplication.hpp"
10
11 #include <fmt/ranges.h>
12
13 #include <imgui_node_editor.h>
14 #include <imgui_node_editor_internal.h>
15 namespace ed = ax::NodeEditor;
16
17 #include <imgui_internal.h>
18 #include <imgui_stdlib.h>
19
20 #include "ImGuiFileDialog.h"
21
22 #include "implot.h"
23 #include <implot_internal.h>
24
25 #include "internal/gui/Shortcuts.hpp"
26 #include "internal/gui/TouchTracker.hpp"
27 #include "internal/gui/FlowAnimation.hpp"
28
29 #include "internal/gui/panels/BlueprintNodeBuilder.hpp"
30 namespace util = ax::NodeEditor::Utilities;
31
32 #include "internal/gui/panels/LeftPane.hpp"
33
34 #include "internal/gui/menus/MainMenuBar.hpp"
35
36 #include "internal/gui/widgets/Splitter.hpp"
37 #include "internal/gui/widgets/HelpMarker.hpp"
38 #include "internal/gui/widgets/Spinner.hpp"
39 #include "internal/gui/widgets/TextAnsiColored.hpp"
40
41 #include "internal/gui/windows/Global.hpp"
42
43 #include "internal/Node/Pin.hpp"
44 #include "internal/Node/Node.hpp"
45
46 #include "internal/NodeManager.hpp"
47 namespace nm = NAV::NodeManager;
48 #include "NodeRegistry.hpp"
49
50 #include "internal/ConfigManager.hpp"
51 #include "internal/FlowManager.hpp"
52 #include "internal/FlowExecutor.hpp"
53
54 #include "util/Json.hpp"
55 #include "util/StringUtil.hpp"
56
57 #include <string>
58 #include <array>
59 #include <vector>
60 #include <map>
61 #include <algorithm>
62 #include <utility>
63 #include <filesystem>
64
65 #include "internal/CMakeRC.hpp"
66
67 namespace NAV::gui
68 {
69 namespace
70 {
71
72 ax::NodeEditor::EditorContext* m_Editor = nullptr;
73
74 } // namespace
75 } // namespace NAV::gui
76
77 void NAV::gui::NodeEditorApplication::OnStart()
78 {
79 LOG_TRACE("called");
80
81 ed::Config config;
82
83 // config.SettingsFile = "INSTINCT.json";
84 // config.UserPointer = this;
85
86 // Stops the Editor from creating a log file, as we do it ourselves
87 // clang-format off
88 config.SaveSettings = [](const char* /*data*/, size_t /*size*/,
89 ed::SaveReasonFlags /*reason*/, void* /*userPointer*/) -> bool {
90 // clang-format on
91
92 return false;
93 };
94
95 // Trigger the changed bar on the node overview list when a node is moved
96 // clang-format off
97 config.SaveNodeSettings = [](ed::NodeId nodeId, const char* /*data*/, size_t /*size*/,
98 ed::SaveReasonFlags /*reason*/, void* /*userPointer*/) -> bool {
99 // clang-format on
100
101 // auto* self = static_cast<NodeEditorApplication*>(userPointer);
102
103 flow::ApplyChanges();
104 gui::TouchNode(nodeId);
105
106 return true;
107 };
108
109 m_Editor = ed::CreateEditor(&config);
110 ed::SetCurrentEditor(m_Editor);
111
112 ImGui::GetStyle().FrameRounding = 4.0F;
113 ed::GetStyle().FlowDuration = 1.0F;
114
115 ImPlot::CreateContext();
116 ImPlot::GetStyle().Use24HourClock = true;
117
118 imPlotReferenceStyle = ImPlot::GetStyle();
119
120 std::filesystem::path imPlotConfigFilepath = flow::GetConfigPath();
121 if (std::filesystem::path inputPath{ ConfigManager::Get<std::string>("implot-config") };
122 inputPath.is_relative())
123 {
124 imPlotConfigFilepath /= inputPath;
125 }
126 else
127 {
128 imPlotConfigFilepath = inputPath;
129 }
130 std::ifstream imPlotConfigFilestream(imPlotConfigFilepath);
131
132 if (!imPlotConfigFilestream.good())
133 {
134 LOG_ERROR("The ImPlot style config file could not be loaded: {}", imPlotConfigFilepath);
135 }
136 else
137 {
138 json j;
139 imPlotConfigFilestream >> j;
140
141 if (j.contains("implot") && j.at("implot").contains("style"))
142 {
143 j.at("implot").at("style").get_to(ImPlot::GetStyle());
144 }
145 }
146
147 // Add custom colormap and set as default. ConfigManager::LoadGlobalSettings then overrides this default selection if it is saved
148 ImPlotContext& gp = *ImPlot::GetCurrentContext();
149 ImVector<ImVec4> custom;
150 for (int c = 0; c < gp.ColormapData.GetKeyCount(ImPlotColormap_Deep); ++c)
151 {
152 custom.push_back(ImGui::ColorConvertU32ToFloat4(gp.ColormapData.GetKeyColor(ImPlotColormap_Deep, c)));
153 }
154 custom.push_back(ImColor(37, 109, 227));
155 custom.push_back(ImColor(239, 100, 21));
156 custom.push_back(ImColor(21, 228, 69));
157 custom.push_back(ImColor(239, 20, 28));
158 custom.push_back(ImColor(100, 64, 217));
159 custom.push_back(ImColor(164, 78, 2));
160 custom.push_back(ImColor(232, 39, 176));
161 custom.push_back(ImColor(255, 207, 31));
162 custom.push_back(ImColor(54, 228, 174));
163 ImPlot::AddColormap("DeepEx", custom.Data, custom.Size, true);
164 ImPlot::BustItemCache();
165 gp.Style.Colormap = gp.ColormapData.Count - 1;
166 ImPlot::BustItemCache();
167
168 ConfigManager::LoadGlobalSettings();
169
170 auto fs = cmrc::instinct::get_filesystem();
171
172 if (fs.is_file("resources/images/BlueprintBackground.png"))
173 {
174 auto fd = fs.open("resources/images/BlueprintBackground.png");
175
176 LOG_DEBUG("Generating Texture for Blueprint Background ({} byte)", fd.size());
177
178 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
179 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
180
181 std::vector<char> buffer;
182 buffer.resize(fd.size(), '\0');
183
184 is.read(buffer.data(),
185 static_cast<std::streamsize>(buffer.size()));
186
187 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
188 m_HeaderBackground = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
189 static_cast<int>(fd.size()));
190 }
191 else
192 {
193 m_HeaderBackground = LoadTexture("resources/images/BlueprintBackground.png");
194 }
195
196 if (fs.is_file("resources/images/INSTINCT_Logo_Text_white_small.png"))
197 {
198 auto fd = fs.open("resources/images/INSTINCT_Logo_Text_white_small.png");
199
200 LOG_DEBUG("Generating Texture for INSTINCT Logo (white) ({} byte)", fd.size());
201
202 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
203 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
204
205 std::vector<char> buffer;
206 buffer.resize(fd.size(), '\0');
207
208 is.read(buffer.data(),
209 static_cast<std::streamsize>(buffer.size()));
210
211 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
212 m_InstinctLogo.at(0) = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
213 static_cast<int>(fd.size()));
214 }
215 else
216 {
217 m_InstinctLogo.at(0) = LoadTexture("resources/images/INSTINCT_Logo_Text_white_small.png");
218 }
219
220 if (fs.is_file("resources/images/INSTINCT_Logo_Text_black_small.png"))
221 {
222 auto fd = fs.open("resources/images/INSTINCT_Logo_Text_black_small.png");
223
224 LOG_DEBUG("Generating Texture for INSTINCT Logo (black) ({} byte)", fd.size());
225
226 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
227 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
228
229 std::vector<char> buffer;
230 buffer.resize(fd.size(), '\0');
231
232 is.read(buffer.data(),
233 static_cast<std::streamsize>(buffer.size()));
234
235 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
236 m_InstinctLogo.at(1) = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
237 static_cast<int>(fd.size()));
238 }
239 else
240 {
241 m_InstinctLogo.at(1) = LoadTexture("resources/images/INSTINCT_Logo_Text_black_small.png");
242 }
243
244 if (fs.is_file("resources/images/INS_logo_rectangular_white_small.png"))
245 {
246 auto fd = fs.open("resources/images/INS_logo_rectangular_white_small.png");
247
248 LOG_DEBUG("Generating Texture for INS Logo (white) ({} byte)", fd.size());
249
250 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
251 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
252
253 std::vector<char> buffer;
254 buffer.resize(fd.size(), '\0');
255
256 is.read(buffer.data(),
257 static_cast<std::streamsize>(buffer.size()));
258
259 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
260 m_InsLogo.at(0) = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
261 static_cast<int>(fd.size()));
262 }
263 else
264 {
265 m_InsLogo.at(0) = LoadTexture("resources/images/INS_logo_rectangular_white_small.png");
266 }
267
268 if (fs.is_file("resources/images/INS_logo_rectangular_black_small.png"))
269 {
270 auto fd = fs.open("resources/images/INS_logo_rectangular_black_small.png");
271
272 LOG_DEBUG("Generating Texture for INS Logo (black) ({} byte)", fd.size());
273
274 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
275 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
276
277 std::vector<char> buffer;
278 buffer.resize(fd.size(), '\0');
279
280 is.read(buffer.data(),
281 static_cast<std::streamsize>(buffer.size()));
282
283 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
284 m_InsLogo.at(1) = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
285 static_cast<int>(fd.size()));
286 }
287 else
288 {
289 m_InsLogo.at(1) = LoadTexture("resources/images/INS_logo_rectangular_black_small.png");
290 }
291
292 if (fs.is_file("resources/images/ic_save_white_24dp.png"))
293 {
294 auto fd = fs.open("resources/images/ic_save_white_24dp.png");
295
296 LOG_DEBUG("Generating Texture for Save icon ({} byte)", fd.size());
297
298 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
299 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
300
301 std::vector<char> buffer;
302 buffer.resize(fd.size(), '\0');
303
304 is.read(buffer.data(),
305 static_cast<std::streamsize>(buffer.size()));
306
307 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
308 m_SaveButtonImage = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
309 static_cast<int>(fd.size()));
310 }
311 else
312 {
313 m_SaveButtonImage = LoadTexture("resources/images/ic_save_white_24dp.png");
314 }
315
316 if (fs.is_file("resources/images/Rose-rhodonea-curve-7x9-chart-improved.jpg"))
317 {
318 auto fd = fs.open("resources/images/Rose-rhodonea-curve-7x9-chart-improved.jpg");
319
320 LOG_DEBUG("Generating Texture for Rose figure ({} byte)", fd.size());
321
322 auto is = cmrc::memstream(const_cast<char*>(fd.begin()), // NOLINT(cppcoreguidelines-pro-type-const-cast)
323 const_cast<char*>(fd.end())); // NOLINT(cppcoreguidelines-pro-type-const-cast)
324
325 std::vector<char> buffer;
326 buffer.resize(fd.size(), '\0');
327
328 is.read(buffer.data(),
329 static_cast<std::streamsize>(buffer.size()));
330
331 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
332 m_RoseFigure = LoadTexture(reinterpret_cast<const void*>(const_cast<const char*>(buffer.data())),
333 static_cast<int>(fd.size()));
334 }
335 else
336 {
337 m_RoseFigure = LoadTexture("resources/images/Rose-rhodonea-curve-7x9-chart-improved.jpg");
338 }
339 }
340
341 void NAV::gui::NodeEditorApplication::OnStop()
342 {
343 LOG_TRACE("called");
344
345 FlowExecutor::stop();
346
347 ConfigManager::SaveGlobalSettings();
348
349 nm::DeleteAllNodes();
350
351 auto releaseTexture = [this](ImTextureID& id) {
352 if (id)
353 {
354 DestroyTexture(id);
355 id = nullptr;
356 }
357 };
358
359 releaseTexture(m_InsLogo.at(0));
360 releaseTexture(m_InsLogo.at(1));
361 releaseTexture(m_InstinctLogo.at(0));
362 releaseTexture(m_InstinctLogo.at(1));
363 releaseTexture(m_HeaderBackground);
364 releaseTexture(m_SaveButtonImage);
365 releaseTexture(m_RoseFigure);
366
367 if (m_Editor)
368 {
369 ed::DestroyEditor(m_Editor);
370 m_Editor = nullptr;
371 }
372 if (ImPlotContext* ctx = ImPlot::GetCurrentContext())
373 {
374 ImPlot::DestroyContext(ctx);
375 }
376 }
377
378 bool NAV::gui::NodeEditorApplication::OnQuitRequest()
379 {
380 LOG_TRACE("called");
381
382 if (flow::HasUnsavedChanges())
383 {
384 globalAction = GlobalActions::QuitUnsaved;
385 return false;
386 }
387
388 return true;
389 }
390
391 void NAV::gui::NodeEditorApplication::ShowQuitRequested()
392 {
393 const auto& io = ImGui::GetIO();
394 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
395 {
396 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
397 {
398 globalAction = GlobalActions::None;
399 return;
400 }
401 }
402
403 ImGui::PushFont(WindowFont());
404 ImGui::OpenPopup("Quit with unsaved changes?");
405 if (ImGui::BeginPopupModal("Quit with unsaved changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
406 {
407 ImGui::Text("Do you want to save your changes or discard them?");
408 if (ImGui::Button("Save"))
409 {
410 if (flow::GetCurrentFilename().empty())
411 {
412 ImGuiFileDialog::Instance()->OpenModal("Save Flow", "Save Flow", ".flow", (flow::GetFlowPath() / ".").string(), 1, nullptr, ImGuiFileDialogFlags_ConfirmOverwrite);
413 ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".flow", ImVec4(0.0F, 1.0F, 0.0F, 0.9F));
414 }
415 else
416 {
417 flow::SaveFlowAs(flow::GetCurrentFilename());
418 globalAction = GlobalActions::None;
419 flow::DiscardChanges();
420 Quit();
421 ImGui::CloseCurrentPopup();
422 }
423 }
424
425 if (ImGuiFileDialog::Instance()->Display("Save Flow", ImGuiWindowFlags_NoCollapse, ImVec2(600, 400)))
426 {
427 if (ImGuiFileDialog::Instance()->IsOk())
428 {
429 flow::SetCurrentFilename(ImGuiFileDialog::Instance()->GetFilePathName());
430 flow::SaveFlowAs(flow::GetCurrentFilename());
431 }
432
433 globalAction = GlobalActions::None;
434 ImGuiFileDialog::Instance()->Close();
435 Quit();
436 ImGui::CloseCurrentPopup();
437 }
438 ImGui::SameLine();
439 if (ImGui::Button("Discard"))
440 {
441 globalAction = GlobalActions::None;
442 flow::DiscardChanges();
443 Quit();
444 ImGui::CloseCurrentPopup();
445 }
446 ImGui::SameLine();
447 if (ImGui::Button("Cancel"))
448 {
449 globalAction = GlobalActions::None;
450 ImGui::CloseCurrentPopup();
451 }
452 ImGui::EndPopup();
453 }
454 ImGui::PopFont();
455 }
456
457 void NAV::gui::NodeEditorApplication::ShowSaveAsRequested()
458 {
459 ImGui::PushFont(WindowFont());
460 ImGuiFileDialog::Instance()->OpenDialog("Save Flow", "Save Flow", ".flow", (flow::GetFlowPath() / ".").string(), 1, nullptr, ImGuiFileDialogFlags_ConfirmOverwrite);
461 ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".flow", ImVec4(0.0F, 1.0F, 0.0F, 0.9F));
462
463 const auto& io = ImGui::GetIO();
464 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
465 {
466 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
467 {
468 globalAction = GlobalActions::None;
469 ImGuiFileDialog::Instance()->Close();
470 ImGui::PopFont();
471 return;
472 }
473 }
474
475 if (ImGuiFileDialog::Instance()->Display("Save Flow", ImGuiWindowFlags_NoCollapse, ImVec2(600, 400)))
476 {
477 if (ImGuiFileDialog::Instance()->IsOk())
478 {
479 flow::SetCurrentFilename(ImGuiFileDialog::Instance()->GetFilePathName());
480 flow::SaveFlowAs(flow::GetCurrentFilename());
481 }
482
483 globalAction = GlobalActions::None;
484 ImGuiFileDialog::Instance()->Close();
485 }
486 ImGui::PopFont();
487 }
488
489 void NAV::gui::NodeEditorApplication::ShowClearNodesRequested()
490 {
491 const auto& io = ImGui::GetIO();
492 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
493 {
494 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
495 {
496 globalAction = GlobalActions::None;
497 return;
498 }
499 }
500
501 ImGui::PushFont(WindowFont());
502 ImGui::OpenPopup("Clear Nodes and discard unsaved changes?");
503 if (ImGui::BeginPopupModal("Clear Nodes and discard unsaved changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
504 {
505 ImGui::Text("Do you want to save your changes before clearing the nodes?");
506 if (ImGui::Button("Save"))
507 {
508 if (flow::GetCurrentFilename().empty())
509 {
510 ImGuiFileDialog::Instance()->OpenModal("Save Flow", "Save Flow", ".flow", (flow::GetFlowPath() / ".").string(), 1, nullptr, ImGuiFileDialogFlags_ConfirmOverwrite);
511 ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".flow", ImVec4(0.0F, 1.0F, 0.0F, 0.9F));
512 }
513 else
514 {
515 flow::SaveFlowAs(flow::GetCurrentFilename());
516 globalAction = GlobalActions::None;
517 nm::DeleteAllNodes();
518 flow::DiscardChanges();
519 flow::SetCurrentFilename("");
520 ImGui::CloseCurrentPopup();
521 }
522 }
523
524 if (ImGuiFileDialog::Instance()->Display("Save Flow", ImGuiWindowFlags_NoCollapse, ImVec2(600, 400)))
525 {
526 if (ImGuiFileDialog::Instance()->IsOk())
527 {
528 flow::SetCurrentFilename(ImGuiFileDialog::Instance()->GetFilePathName());
529 flow::SaveFlowAs(flow::GetCurrentFilename());
530
531 ImGuiFileDialog::Instance()->Close();
532 nm::DeleteAllNodes();
533 flow::DiscardChanges();
534 flow::SetCurrentFilename("");
535 }
536
537 globalAction = GlobalActions::None;
538
539 ImGui::CloseCurrentPopup();
540 }
541 ImGui::SameLine();
542 if (ImGui::Button("Discard"))
543 {
544 globalAction = GlobalActions::None;
545 nm::DeleteAllNodes();
546 flow::DiscardChanges();
547 flow::SetCurrentFilename("");
548 ImGui::CloseCurrentPopup();
549 }
550 ImGui::SameLine();
551 if (ImGui::Button("Cancel"))
552 {
553 globalAction = GlobalActions::None;
554 ImGui::CloseCurrentPopup();
555 }
556 ImGui::EndPopup();
557 }
558 ImGui::PopFont();
559 }
560
561 void NAV::gui::NodeEditorApplication::ShowLoadRequested()
562 {
563 ImGui::PushFont(WindowFont());
564 if (flow::HasUnsavedChanges())
565 {
566 ImGui::OpenPopup("Discard unsaved changes?");
567 if (ImGui::BeginPopupModal("Discard unsaved changes?", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
568 {
569 ImGui::Text("Do you want to save your changes or discard them before loading?");
570 if (ImGui::Button("Save"))
571 {
572 if (flow::GetCurrentFilename().empty())
573 {
574 ImGuiFileDialog::Instance()->OpenModal("Save Flow##Load", "Save Flow", ".flow", (flow::GetFlowPath() / ".").string(), 1, nullptr, ImGuiFileDialogFlags_ConfirmOverwrite);
575 ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".flow", ImVec4(0.0F, 1.0F, 0.0F, 0.9F));
576 }
577 else
578 {
579 flow::SaveFlowAs(flow::GetCurrentFilename());
580 flow::DiscardChanges();
581 ImGui::CloseCurrentPopup();
582 }
583 }
584
585 if (ImGuiFileDialog::Instance()->Display("Save Flow##Load", ImGuiWindowFlags_NoCollapse, ImVec2(600, 400)))
586 {
587 if (ImGuiFileDialog::Instance()->IsOk())
588 {
589 flow::SetCurrentFilename(ImGuiFileDialog::Instance()->GetFilePathName());
590 flow::SaveFlowAs(flow::GetCurrentFilename());
591 }
592
593 flow::DiscardChanges();
594 ImGuiFileDialog::Instance()->Close();
595 ImGui::CloseCurrentPopup();
596 }
597 ImGui::SameLine();
598 if (ImGui::Button("Discard"))
599 {
600 flow::DiscardChanges();
601 ImGui::CloseCurrentPopup();
602 }
603 ImGui::SameLine();
604 if (ImGui::Button("Cancel"))
605 {
606 globalAction = GlobalActions::None;
607 ImGui::CloseCurrentPopup();
608 }
609 ImGui::EndPopup();
610 }
611 }
612 else
613 {
614 ImGuiFileDialog::Instance()->OpenDialog("Load Flow", "Load Flow", ".flow", (flow::GetFlowPath() / ".").string(), 1, nullptr);
615 ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtention, ".flow", ImVec4(0.0F, 1.0F, 0.0F, 0.9F));
616
617 static bool loadSuccessful = true;
618
619 auto& io = ImGui::GetIO();
620 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
621 {
622 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
623 {
624 globalAction = GlobalActions::None;
625 loadSuccessful = true;
626 ImGuiFileDialog::Instance()->Close();
627 ImGui::PopFont();
628 return;
629 }
630 }
631
632 if (ImGuiFileDialog::Instance()->Display("Load Flow", ImGuiWindowFlags_NoCollapse, ImVec2(600, 400)))
633 {
634 if (ImGuiFileDialog::Instance()->IsOk())
635 {
636 loadSuccessful = flow::LoadFlow(ImGuiFileDialog::Instance()->GetFilePathName());
637 if (loadSuccessful)
638 {
639 globalAction = GlobalActions::None;
640 frameCountNavigate = ImGui::GetFrameCount();
641 ImGuiFileDialog::Instance()->Close();
642 }
643 }
644 else
645 {
646 globalAction = GlobalActions::None;
647 ImGuiFileDialog::Instance()->Close();
648 }
649 }
650 if (!loadSuccessful)
651 {
652 ImGui::OpenPopup("Could not open file");
653 }
654
655 if (ImGui::BeginPopupModal("Could not open file", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize))
656 {
657 ImGui::Text("Could not open the flow file. This can have the following reasons:");
658 ImGui::Text("- the filename specified is invalid");
659 ImGui::Text("- the program has insufficient permissions to access the file");
660 ImGui::Text("- the provided file was not a valid flow file");
661 ImGui::Text("Read the log output for further information");
662 if (ImGui::Button("Close"))
663 {
664 loadSuccessful = true;
665 ImGui::CloseCurrentPopup();
666 }
667 ImGui::EndPopup();
668 }
669 }
670 ImGui::PopFont();
671 }
672
673 void NAV::gui::NodeEditorApplication::ShowRenameNodeRequest(Node*& renameNode)
674 {
675 ImGui::PushFont(WindowFont());
676 const char* title = renameNode->kind == Node::Kind::GroupBox ? "Rename Group Box" : "Rename Node";
677 ImGui::OpenPopup(title);
678 if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_AlwaysAutoResize))
679 {
680 static std::string nameBackup = renameNode->name;
681 if (nameBackup.empty())
682 {
683 nameBackup = renameNode->name;
684 }
685
686 auto& io = ImGui::GetIO();
687 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
688 {
689 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
690 {
691 if (renameNode)
692 {
693 renameNode->name = nameBackup;
694 }
695 nameBackup.clear();
696 renameNode = nullptr;
697 ImGui::CloseCurrentPopup();
698 ImGui::EndPopup();
699 ImGui::PopFont();
700 return;
701 }
702 }
703
704 if (ImGui::InputTextMultiline(fmt::format("##{}", size_t(renameNode->id)).c_str(), &renameNode->name, ImVec2(0, 65), ImGuiInputTextFlags_CtrlEnterForNewLine | ImGuiInputTextFlags_EnterReturnsTrue))
705 {
706 nameBackup.clear();
707 renameNode = nullptr;
708 ImGui::CloseCurrentPopup();
709 }
710 ImGui::SameLine();
711 gui::widgets::HelpMarker("Hold SHIFT or use mouse to select text.\n"
712 "CTRL+Left/Right to word jump.\n"
713 "CTRL+A or double-click to select all.\n"
714 "CTRL+X,CTRL+C,CTRL+V clipboard.\n"
715 "CTRL+Z,CTRL+Y undo/redo.\n"
716 "ESCAPE to revert.");
717 if (ImGui::Button("Accept"))
718 {
719 nameBackup.clear();
720 renameNode = nullptr;
721 flow::ApplyChanges();
722 ImGui::CloseCurrentPopup();
723 }
724 ImGui::SameLine();
725 if (ImGui::Button("Cancel"))
726 {
727 if (renameNode)
728 {
729 renameNode->name = nameBackup;
730 }
731 nameBackup.clear();
732 renameNode = nullptr;
733 ImGui::CloseCurrentPopup();
734 }
735 ImGui::EndPopup();
736 }
737 ImGui::PopFont();
738 }
739
740 void NAV::gui::NodeEditorApplication::ShowRenamePinRequest(Pin*& renamePin)
741 {
742 ImGui::PushFont(WindowFont());
743 const char* title = "Rename Pin";
744 ImGui::OpenPopup(title);
745 if (ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_AlwaysAutoResize))
746 {
747 static std::string nameBackup = renamePin->name;
748 if (nameBackup.empty())
749 {
750 nameBackup = renamePin->name;
751 }
752
753 auto& io = ImGui::GetIO();
754 if (!io.KeyCtrl && !io.KeyAlt && !io.KeyShift && !io.KeySuper)
755 {
756 if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
757 {
758 if (renamePin)
759 {
760 renamePin->name = nameBackup;
761 }
762 nameBackup.clear();
763 renamePin = nullptr;
764 ImGui::CloseCurrentPopup();
765 ImGui::EndPopup();
766 ImGui::PopFont();
767 return;
768 }
769 }
770
771 if (ImGui::InputTextMultiline(fmt::format("##{}", size_t(renamePin->id)).c_str(), &renamePin->name, ImVec2(0, 65), ImGuiInputTextFlags_CtrlEnterForNewLine | ImGuiInputTextFlags_EnterReturnsTrue))
772 {
773 nameBackup.clear();
774 renamePin = nullptr;
775 ImGui::CloseCurrentPopup();
776 }
777 ImGui::SameLine();
778 gui::widgets::HelpMarker("Hold SHIFT or use mouse to select text.\n"
779 "CTRL+Left/Right to word jump.\n"
780 "CTRL+A or double-click to select all.\n"
781 "CTRL+X,CTRL+C,CTRL+V clipboard.\n"
782 "CTRL+Z,CTRL+Y undo/redo.\n"
783 "ESCAPE to revert.");
784 if (ImGui::Button("Accept"))
785 {
786 nameBackup.clear();
787 renamePin = nullptr;
788 flow::ApplyChanges();
789 ImGui::CloseCurrentPopup();
790 }
791 ImGui::SameLine();
792 if (ImGui::Button("Cancel"))
793 {
794 if (renamePin)
795 {
796 renamePin->name = nameBackup;
797 }
798 nameBackup.clear();
799 renamePin = nullptr;
800 ImGui::CloseCurrentPopup();
801 }
802 ImGui::EndPopup();
803 }
804 ImGui::PopFont();
805 }
806
807 void NAV::gui::NodeEditorApplication::OnFrame(float deltaTime)
808 {
809 bool firstFrame = ImGui::GetFrameCount() == 1;
810
811 if (frameCountNavigate && ImGui::GetFrameCount() - frameCountNavigate > 3)
812 {
813 frameCountNavigate = 0;
814 ed::NavigateToContent();
815 }
816
817 switch (globalAction)
818 {
819 case GlobalActions::Quit:
820 Quit();
821 break;
822 case GlobalActions::QuitUnsaved:
823 ShowQuitRequested();
824 break;
825 case GlobalActions::SaveAs:
826 ShowSaveAsRequested();
827 break;
828 case GlobalActions::Clear:
829 ShowClearNodesRequested();
830 break;
831 case GlobalActions::Load:
832 ShowLoadRequested();
833 break;
834 default:
835 break;
836 }
837
838 gui::UpdateTouch(deltaTime);
839
840 if (ed::AreShortcutsEnabled())
841 {
842 gui::checkShortcuts(globalAction);
843 }
844
845 ImGui::PushFont(PanelFont());
846 gui::menus::ShowMainMenuBar(globalAction);
847 menuBarHeight = ImGui::GetCursorPosY();
848 ImGui::PopFont();
849
850 ed::SetCurrentEditor(m_Editor);
851
852 static ed::NodeId contextNodeId = 0;
853 static ed::LinkId contextLinkId = 0;
854 static ed::PinId contextPinId = 0;
855 static bool createNewNode = false;
856 static Pin* newNodeLinkPin = nullptr;
857
858 bool leftPaneActive = false;
859 if (!hideLeftPane)
860 {
861 gui::widgets::Splitter("Main Splitter", true, SPLITTER_THICKNESS, &leftPaneWidth, &rightPaneWidth, 25.0F, 50.0F);
862
863 ImGui::PushFont(PanelFont());
864 leftPaneActive = gui::panels::ShowLeftPane(leftPaneWidth - SPLITTER_THICKNESS);
865 ImGui::PopFont();
866
867 ImGui::SameLine(0.0F, 12.0F);
868 }
869
870 // ToolTips have to be shown outside of the NodeEditor Context, so save the Tooltip and push it afterwards
871 std::string tooltipText;
872
873 ImGui::BeginGroup();
874
875 bool darkMode = ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_Bg].x
876 + ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_Bg].y
877 + ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_Bg].z
878 > 2.0F;
879
880 if (bottomViewSelectedTab != BottomViewTabItem::None)
881 {
882 float blueprintHeight = ImGui::GetContentRegionAvail().y - bottomViewHeight + (isUsingBigPanelFont() ? 48.5F : 28.5F);
883 ImGui::PushStyleColor(ImGuiCol_Separator, IM_COL32_BLACK_TRANS);
884 gui::widgets::Splitter("Log Splitter", false, 6.0F, &blueprintHeight, &bottomViewHeight, 400.0F, BOTTOM_VIEW_UNCOLLAPSED_MIN_HEIGHT);
885 ImGui::PopStyleColor();
886 }
887
888 ed::Begin("Node editor", ImVec2(0, ImGui::GetContentRegionAvail().y - bottomViewHeight + SPLITTER_THICKNESS));
889 {
890 static Pin* newLinkPin = nullptr;
891
892 auto cursorTopLeft = ImGui::GetCursorScreenPos();
893
894 static util::BlueprintNodeBuilder builder(m_HeaderBackground, GetTextureWidth(m_HeaderBackground), GetTextureHeight(m_HeaderBackground));
895
896 auto textColor = ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_NodeBg].x
897 + ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_NodeBg].y
898 + ax::NodeEditor::GetStyle().Colors[ax::NodeEditor::StyleColor_NodeBg].z
899 > 2.0F
900 ? IM_COL32(0, 0, 0, 255)
901 : IM_COL32(255, 255, 255, 255);
902
903 for (auto* node : nm::m_Nodes()) // Blueprint || Simple
904 {
905 if (node->kind != Node::Kind::Blueprint && node->kind != Node::Kind::Simple) // NOLINT(misc-redundant-expression) - false positive warning
906 {
907 continue;
908 }
909
910 const auto isSimple = node->kind == Node::Kind::Simple;
911
912 bool hasOutputDelegates = false;
913 for (const auto& output : node->outputPins)
914 {
915 if (output.type == Pin::Type::Delegate)
916 {
917 hasOutputDelegates = true;
918 }
919 }
920
921 builder.Begin(node->id);
922
923 if (!isSimple) // Header Text for Blueprint Nodes
924 {
925 if (node->isDisabled()) // Node disabled
926 {
927 builder.Header(ImColor(192, 192, 192)); // Silver
928 }
929 else if (node->isInitialized())
930 {
931 builder.Header(ImColor(128, 255, 128)); // Light green
932 }
933 else
934 {
935 builder.Header(ImColor(255, 128, 128)); // Light red
936 }
937 ImGui::Spring(0);
938 ImGui::PushStyleColor(ImGuiCol_Text, textColor);
939 ImGui::TextUnformatted(node->name.c_str());
940 ImGui::PopStyleColor();
941 ImGui::Spring(1);
942 if (node->getState() == Node::State::DoInitialize)
943 {
944 gui::widgets::Spinner(("##Spinner " + node->nameId()).c_str(), ImColor(144, 202, 238), 10.0F, 1.0F);
945 }
946 else if (node->getState() == Node::State::Initializing)
947 {
948 gui::widgets::Spinner(("##Spinner " + node->nameId()).c_str(), ImColor(144, 238, 144), 10.0F, 1.0F);
949 }
950 else if (node->getState() == Node::State::DoDeinitialize)
951 {
952 gui::widgets::Spinner(("##Spinner " + node->nameId()).c_str(), ImColor(255, 222, 122), 10.0F, 1.0F);
953 }
954 else if (node->getState() == Node::State::Deinitializing)
955 {
956 gui::widgets::Spinner(("##Spinner " + node->nameId()).c_str(), ImColor(255, 160, 122), 10.0F, 1.0F);
957 }
958 else
959 {
960 ImGui::Dummy(ImVec2(ImGui::GetStyle().ItemSpacing.x + 12.0F + ImGui::GetStyle().FramePadding.x, 26));
961 }
962 if (hasOutputDelegates)
963 {
964 ImGui::BeginVertical("delegates", ImVec2(0, 26));
965 ImGui::Spring(1, 0);
966 for (const auto& output : node->outputPins)
967 {
968 if (output.type != Pin::Type::Delegate)
969 {
970 continue;
971 }
972
973 auto alpha = ImGui::GetStyle().Alpha;
974 if (newLinkPin && newLinkPin->kind == Pin::Kind::Input && &output != newLinkPin
975 && !reinterpret_cast<InputPin*>(newLinkPin)->canCreateLink(output))
976 {
977 alpha = alpha * (48.0F / 255.0F);
978 }
979
980 ed::BeginPin(output.id, ed::PinKind::Output);
981 ed::PinPivotAlignment(ImVec2(1.0F, 0.5F));
982 ed::PinPivotSize(ImVec2(0, 0));
983 ImGui::BeginHorizontal(output.id.AsPointer());
984 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
985 // if (!output.name.empty())
986 // {
987 // ImGui::PushStyleColor(ImGuiCol_Text, textColor);
988 // ImGui::TextUnformatted(output.name.c_str());
989 // ImGui::PopStyleColor();
990 // ImGui::Spring(0);
991 // }
992 output.drawPinIcon(output.isPinLinked(), static_cast<int>(alpha * 255));
993 ImGui::Spring(0, ImGui::GetStyle().ItemSpacing.x / 2);
994 ImGui::EndHorizontal();
995 ImGui::PopStyleVar();
996 ed::EndPin();
997
998 // DrawItemRect(ImColor(255, 0, 0));
999 }
1000 ImGui::Spring(1, 0);
1001 ImGui::EndVertical();
1002 ImGui::Spring(0, ImGui::GetStyle().ItemSpacing.x / 2);
1003 }
1004 else
1005 {
1006 ImGui::Spring(0);
1007 }
1008 builder.EndHeader();
1009 }
1010
1011 for (auto& input : node->inputPins) // Input Pins
1012 {
1013 auto alpha = ImGui::GetStyle().Alpha;
1014 if (newLinkPin && newLinkPin->kind == Pin::Kind::Output && &input != newLinkPin
1015 && !reinterpret_cast<OutputPin*>(newLinkPin)->canCreateLink(input))
1016 {
1017 alpha = alpha * (48.0F / 255.0F);
1018 }
1019
1020 builder.Input(input.id);
1021 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
1022 input.drawPinIcon(input.isPinLinked(), static_cast<int>(alpha * 255));
1023 if (ImGui::IsItemHovered()) { tooltipText = fmt::format("{}", fmt::join(input.dataIdentifier, "\n")); }
1024 if (_showQueueSizeOnPins && input.type == Pin::Type::Flow && input.isPinLinked())
1025 {
1026 auto cursor = ImGui::GetCursorPos();
1027 std::string text = fmt::format("{}{}", input.queue.size(), input.queueBlocked ? "*" : "");
1028 auto* drawList = ImGui::GetWindowDrawList();
1029 drawList->AddText(ImVec2(cursor.x - 26.0F, cursor.y + 2.F), IM_COL32(255, 0, 0, 255), text.c_str());
1030 }
1031
1032 ImGui::Spring(0);
1033 if (!input.name.empty())
1034 {
1035 bool noneType = input.type == Pin::Type::None;
1036 if (noneType)
1037 {
1038 ImGui::BeginDisabled();
1039 }
1040 ImGui::PushStyleColor(ImGuiCol_Text, textColor);
1041 ImGui::TextUnformatted(input.name.c_str());
1042 ImGui::PopStyleColor();
1043 if (noneType)
1044 {
1045 ImGui::EndDisabled();
1046 }
1047 ImGui::Spring(0);
1048 }
1049 ImGui::PopStyleVar();
1050 util::BlueprintNodeBuilder::EndInput();
1051 }
1052
1053 if (isSimple) // Middle Text for Simple Nodes
1054 {
1055 builder.Middle();
1056
1057 ImGui::Spring(1, 0);
1058 ImGui::PushStyleColor(ImGuiCol_Text, textColor);
1059 ImGui::TextUnformatted(node->name.c_str());
1060 ImGui::PopStyleColor();
1061 ImGui::Spring(1, 0);
1062 }
1063
1064 for (const auto& output : node->outputPins) // Output Pins
1065 {
1066 if (!isSimple && output.type == Pin::Type::Delegate)
1067 {
1068 continue;
1069 }
1070
1071 auto alpha = ImGui::GetStyle().Alpha;
1072 if (newLinkPin && newLinkPin->kind == Pin::Kind::Input && &output != newLinkPin
1073 && !reinterpret_cast<InputPin*>(newLinkPin)->canCreateLink(output))
1074 {
1075 alpha = alpha * (48.0F / 255.0F);
1076 }
1077
1078 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
1079 builder.Output(output.id);
1080 if (!output.name.empty())
1081 {
1082 ImGui::Spring(0);
1083 bool noneType = output.type == Pin::Type::None;
1084 if (noneType)
1085 {
1086 ImGui::BeginDisabled();
1087 }
1088 ImGui::PushStyleColor(ImGuiCol_Text, textColor);
1089 ImGui::TextUnformatted(output.name.c_str());
1090 ImGui::PopStyleColor();
1091 if (noneType)
1092 {
1093 ImGui::EndDisabled();
1094 }
1095 }
1096 ImGui::Spring(0);
1097 output.drawPinIcon(output.isPinLinked(), static_cast<int>(alpha * 255));
1098 if (ImGui::IsItemHovered()) { tooltipText = fmt::format("{}", fmt::join(output.dataIdentifier, "\n")); }
1099 if (_showQueueSizeOnPins && output.type == Pin::Type::Flow && output.isPinLinked())
1100 {
1101 auto cursor = ImGui::GetCursorPos();
1102 std::string text = fmt::format("{}", output.noMoreDataAvailable ? "F" : "P");
1103 auto* drawList = ImGui::GetWindowDrawList();
1104 drawList->AddText(ImVec2(cursor.x - 26.0F, cursor.y + 2.F), IM_COL32(255, 0, 0, 255), text.c_str());
1105 }
1106
1107 ImGui::PopStyleVar();
1108 util::BlueprintNodeBuilder::EndOutput();
1109 }
1110
1111 builder.End();
1112 }
1113
1114 for (const auto* node : nm::m_Nodes()) // GroupBox
1115 {
1116 if (node->kind != Node::Kind::GroupBox)
1117 {
1118 continue;
1119 }
1120
1121 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.75F);
1122 ed::PushStyleColor(ed::StyleColor_NodeBg, m_colors[COLOR_GROUP_HEADER_BG]);
1123 ed::PushStyleColor(ed::StyleColor_NodeBorder, m_colors[COLOR_GROUP_OUTER_BORDER]);
1124 ed::BeginNode(node->id);
1125 ImGui::PushID(node->id.AsPointer());
1126 ImGui::BeginVertical("content");
1127 ImGui::BeginHorizontal("horizontal");
1128 ImGui::Spring(1);
1129 ImGui::PushStyleColor(ImGuiCol_Text, m_colors[COLOR_GROUP_HEADER_TEXT]);
1130 ImGui::TextUnformatted(node->name.c_str());
1131 ImGui::PopStyleColor();
1132 ImGui::Spring(1);
1133 ImGui::EndHorizontal();
1134 ed::Group(node->_size);
1135 ImGui::EndVertical();
1136 ImGui::PopID();
1137 ed::EndNode();
1138 ed::PopStyleColor(2);
1139 ImGui::PopStyleVar();
1140 }
1141
1142 for (const auto* node : nm::m_Nodes())
1143 {
1144 for (const auto& output : node->outputPins) // Output Pins
1145 {
1146 auto color = output.getIconColor();
1147 if (output.type == Pin::Type::Flow)
1148 {
1149 color = darkMode ? ImColor{ 0, 0, 0 } : ImColor{ 255, 255, 255 };
1150 }
1151
1152 for (const auto& link : output.links)
1153 {
1154 ed::Link(link.linkId, output.id, link.connectedPinId, color, 2.0F * defaultFontRatio());
1155 }
1156 }
1157 }
1158
1159 if (!createNewNode)
1160 {
1161 if (ed::BeginCreate(ImColor(255, 255, 255), 2.0F))
1162 {
1163 auto showLabel = [](const char* label, ImColor color) {
1164 ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight());
1165 auto size = ImGui::CalcTextSize(label);
1166
1167 auto padding = ImGui::GetStyle().FramePadding;
1168 auto spacing = ImGui::GetStyle().ItemSpacing;
1169
1170 ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(spacing.x, -spacing.y));
1171
1172 auto rectMin = ImGui::GetCursorScreenPos() - padding;
1173 auto rectMax = ImGui::GetCursorScreenPos() + size + padding;
1174
1175 auto* drawList = ImGui::GetWindowDrawList();
1176 drawList->AddRectFilled(rectMin, rectMax, color, size.y * 0.15F);
1177 ImGui::TextUnformatted(label);
1178 };
1179
1180 ed::PinId startPinId = 0;
1181 ed::PinId endPinId = 0;
1182 if (ed::QueryNewLink(&startPinId, &endPinId))
1183 {
1184 Pin* startPin = nm::FindInputPin(startPinId);
1185 Pin* endPin = nm::FindInputPin(endPinId);
1186 if (!startPin) { startPin = nm::FindOutputPin(startPinId); }
1187 if (!endPin) { endPin = nm::FindOutputPin(endPinId); }
1188
1189 newLinkPin = startPin ? startPin : endPin;
1190
1191 if (startPin && startPin->kind == Pin::Kind::Input)
1192 {
1193 std::swap(startPin, endPin);
1194 std::swap(startPinId, endPinId);
1195 }
1196
1197 if (startPin && endPin)
1198 {
1199 if (endPin == startPin)
1200 {
1201 ed::RejectNewItem(ImColor(255, 0, 0), 2.0F);
1202 }
1203 else if (endPin->kind == startPin->kind)
1204 {
1205 showLabel("x Incompatible Pin Kind", ImColor(45, 32, 32, 180));
1206 ed::RejectNewItem(ImColor(255, 0, 0), 2.0F);
1207 }
1208 else if (endPin->parentNode == startPin->parentNode)
1209 {
1210 showLabel("x Cannot connect to self", ImColor(45, 32, 32, 180));
1211 ed::RejectNewItem(ImColor(255, 0, 0), 1.0F);
1212 }
1213 else if (endPin->type != startPin->type)
1214 {
1215 showLabel("x Incompatible Pin Type", ImColor(45, 32, 32, 180));
1216 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1217 }
1218 else if (reinterpret_cast<InputPin*>(endPin)->isPinLinked())
1219 {
1220 showLabel("End Pin already linked", ImColor(45, 32, 32, 180));
1221 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1222 }
1223 else if (startPin->type == Pin::Type::Flow
1224 && !NAV::NodeRegistry::NodeDataTypeAnyIsChildOf(startPin->dataIdentifier, endPin->dataIdentifier))
1225 {
1226 showLabel(fmt::format("The data type [{}]\ncan't be linked to [{}]",
1227 fmt::join(startPin->dataIdentifier, ","),
1228 fmt::join(endPin->dataIdentifier, ","))
1229 .c_str(),
1230 ImColor(45, 32, 32, 180));
1231 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1232 }
1233 else if (startPin->type == Pin::Type::Delegate
1234 && (startPin->parentNode == nullptr
1235 || std::ranges::find(endPin->dataIdentifier, startPin->parentNode->type()) == endPin->dataIdentifier.end()))
1236 {
1237 if (startPin->parentNode != nullptr)
1238 {
1239 showLabel(fmt::format("The delegate type [{}]\ncan't be linked to [{}]",
1240 startPin->parentNode->type(),
1241 fmt::join(endPin->dataIdentifier, ","))
1242 .c_str(),
1243 ImColor(45, 32, 32, 180));
1244 }
1245
1246 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1247 }
1248 else if ((startPin->type == Pin::Type::Object || startPin->type == Pin::Type::Matrix) // NOLINT(misc-redundant-expression) - false positive warning
1249 && !Pin::dataIdentifierHaveCommon(startPin->dataIdentifier, endPin->dataIdentifier))
1250 {
1251 showLabel(fmt::format("The data type [{}]\ncan't be linked to [{}]",
1252 fmt::join(startPin->dataIdentifier, ","),
1253 fmt::join(endPin->dataIdentifier, ","))
1254 .c_str(),
1255 ImColor(45, 32, 32, 180));
1256 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1257 }
1258 else
1259 {
1260 showLabel("+ Create Link", ImColor(32, 45, 32, 180));
1261 if (ed::AcceptNewItem(ImColor(128, 255, 128), 4.0F))
1262 {
1263 reinterpret_cast<OutputPin*>(startPin)->createLink(*reinterpret_cast<InputPin*>(endPin));
1264 }
1265 }
1266 }
1267 }
1268
1269 ed::PinId pinId = 0;
1270 if (ed::QueryNewNode(&pinId))
1271 {
1272 newLinkPin = nm::FindInputPin(pinId);
1273 if (!newLinkPin) { newLinkPin = nm::FindOutputPin(pinId); }
1274
1275 if (newLinkPin && newLinkPin->kind == Pin::Kind::Input && reinterpret_cast<InputPin*>(newLinkPin)->isPinLinked())
1276 {
1277 showLabel("End Pin already linked", ImColor(45, 32, 32, 180));
1278 ed::RejectNewItem(ImColor(255, 128, 128), 1.0F);
1279 }
1280 else
1281 {
1282 if (newLinkPin)
1283 {
1284 showLabel("+ Create Node", ImColor(32, 45, 32, 180));
1285 }
1286
1287 if (ed::AcceptNewItem())
1288 {
1289 createNewNode = true;
1290 newNodeLinkPin = nm::FindInputPin(pinId);
1291 if (!newNodeLinkPin) { newNodeLinkPin = nm::FindOutputPin(pinId); }
1292 newLinkPin = nullptr;
1293 ed::Suspend();
1294 ImGui::OpenPopup("Create New Node");
1295 ed::Resume();
1296 }
1297 }
1298 }
1299 }
1300 else
1301 {
1302 newLinkPin = nullptr;
1303 }
1304
1305 ed::EndCreate();
1306
1307 if (ed::BeginDelete())
1308 {
1309 ed::LinkId linkId = 0;
1310 while (ed::QueryDeletedLink(&linkId))
1311 {
1312 if (ed::AcceptDeletedItem())
1313 {
1314 bool deleted = false;
1315 for (auto* node : nm::m_Nodes())
1316 {
1317 if (deleted) { break; }
1318 for (auto& output : node->outputPins)
1319 {
1320 if (deleted) { break; }
1321 for (const auto& link : output.links)
1322 {
1323 if (link.linkId == linkId)
1324 {
1325 output.deleteLink(*link.getConnectedPin());
1326 deleted = true;
1327 break;
1328 }
1329 }
1330 }
1331 }
1332 }
1333 }
1334
1335 ed::NodeId nodeId = 0;
1336 while (ed::QueryDeletedNode(&nodeId))
1337 {
1338 if (Node* node = nm::FindNode(nodeId))
1339 {
1340 if (node->isTransient())
1341 {
1342 break;
1343 }
1344 }
1345 if (ed::AcceptDeletedItem())
1346 {
1347 nm::DeleteNode(nodeId);
1348 }
1349 }
1350 }
1351 ed::EndDelete();
1352 }
1353
1354 ImGui::SetCursorScreenPos(cursorTopLeft);
1355 }
1356
1357 // Shortcut enable/disable
1358 ax::NodeEditor::EnableShortcuts(ed::IsActive() || leftPaneActive);
1359
1360 // auto openPopupPosition = ImGui::GetMousePos();
1361 ed::Suspend();
1362 if (ed::ShowNodeContextMenu(&contextNodeId))
1363 {
1364 ImGui::OpenPopup("Node Context Menu");
1365 }
1366 else if (ed::ShowPinContextMenu(&contextPinId))
1367 {
1368 ImGui::OpenPopup("Pin Context Menu");
1369 }
1370 else if (ed::ShowLinkContextMenu(&contextLinkId) && ed::IsActive())
1371 {
1372 ImGui::OpenPopup("Link Context Menu");
1373 }
1374 else if (ed::ShowBackgroundContextMenu() && ed::IsActive())
1375 {
1376 ImGui::OpenPopup("Create New Node");
1377 newNodeLinkPin = nullptr;
1378 }
1379 else if (ed::NodeId doubleClickedNodeId = ed::GetDoubleClickedNode())
1380 {
1381 Node* node = nm::FindNode(doubleClickedNodeId);
1382 node->_showConfig = true;
1383 node->_configWindowFocus = true;
1384 }
1385 ed::Resume();
1386
1387 ed::Suspend();
1388 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
1389 static Node* renameNode = nullptr;
1390 static Pin* renamePin = nullptr;
1391 if (ImGui::BeginPopup("Node Context Menu"))
1392 {
1393 auto* node = nm::FindNode(contextNodeId);
1394
1395 ImGui::TextUnformatted("Node Context Menu");
1396 ImGui::Separator();
1397 if (node)
1398 {
1399 ImGui::Text("ID: %lu", size_t(node->id));
1400 ImGui::Text("Type: %s", node->type().c_str());
1401 ImGui::Text("Kind: %s", std::string(node->kind).c_str());
1402 ImGui::Text("Inputs: %lu", node->inputPins.size());
1403 ImGui::Text("Outputs: %lu", node->outputPins.size());
1404 ImGui::Text("State: %s", Node::toString(node->getState()).c_str());
1405 ImGui::Text("Mode: %s", node->getMode() == Node::Mode::POST_PROCESSING ? "Post-processing" : "Real-time");
1406 ImGui::Separator();
1407 if (node->kind != Node::Kind::GroupBox)
1408 {
1409 if (ImGui::MenuItem(node->isInitialized() ? "Reinitialize" : "Initialize", "",
1410 false, node->isInitialized() || node->getState() == Node::State::Deinitialized))
1411 {
1412 if (node->isInitialized()) { node->doReinitialize(); }
1413 else { node->doInitialize(); }
1414 }
1415 if (ImGui::MenuItem("Deinitialize", "", false, node->isInitialized()))
1416 {
1417 node->doDeinitialize();
1418 }
1419 if (ImGui::MenuItem("Wake Worker"))
1420 {
1421 node->wakeWorker();
1422 }
1423 ImGui::Separator();
1424 if (node->_hasConfig && ImGui::MenuItem("Configure", "", false))
1425 {
1426 node->_showConfig = true;
1427 node->_configWindowFocus = true;
1428 }
1429 if (ImGui::MenuItem(node->isDisabled() ? "Enable" : "Disable", "", false, !node->isTransient()))
1430 {
1431 if (node->isDisabled())
1432 {
1433 node->doEnable();
1434 }
1435 else
1436 {
1437 node->doDisable();
1438 }
1439 flow::ApplyChanges();
1440 }
1441 }
1442 if (ImGui::MenuItem("Rename"))
1443 {
1444 renameNode = node;
1445 }
1446 ImGui::Separator();
1447 if (ImGui::MenuItem("Delete", "", false, !node->isTransient()))
1448 {
1449 ed::DeleteNode(contextNodeId);
1450 }
1451 }
1452 else
1453 {
1454 ImGui::Text("Unknown node: %lu", size_t(contextNodeId));
1455 }
1456
1457 ImGui::EndPopup();
1458 }
1459
1460 if (renameNode) // Popup for renaming a node
1461 {
1462 ShowRenameNodeRequest(renameNode);
1463 }
1464
1465 if (ImGui::BeginPopup("Pin Context Menu"))
1466 {
1467 ImGui::TextUnformatted("Pin Context Menu");
1468 ImGui::Separator();
1469 if (auto* pin = nm::FindInputPin(contextPinId))
1470 {
1471 ImGui::Text("ID: %lu", size_t(pin->id));
1472 ImGui::Text("Node: %s", pin->parentNode ? std::to_string(size_t(pin->parentNode->id)).c_str() : "<none>");
1473 ImGui::Text("Type: %s", std::string(pin->type).c_str());
1474 ImGui::Separator();
1475 if (pin->isPinLinked())
1476 {
1477 const auto& link = pin->link;
1478 ImGui::SetNextItemOpen(true, ImGuiCond_Appearing);
1479 if (ImGui::TreeNode(fmt::format("LinkId: {}", size_t(link.linkId)).c_str()))
1480 {
1481 ImGui::BulletText("Connected Node: %s", link.connectedNode->nameId().c_str());
1482 ImGui::BulletText("Connected Pin: %s (%zu)", link.getConnectedPin()->name.c_str(),
1483 size_t(link.getConnectedPin()->id));
1484 ImGui::TreePop();
1485 }
1486 }
1487 else { ImGui::TextUnformatted("Link: Not linked"); }
1488 ImGui::Separator();
1489 if (ImGui::TreeNode(fmt::format("Queue: {}", pin->queue.size()).c_str()))
1490 {
1491 ImGui::BeginChild("QueueItems", ImVec2(ImGui::GetContentRegionAvail().x, 150), false, ImGuiWindowFlags_None);
1492 for (size_t i = 0; i < pin->queue.size(); i++) // NOLINT(modernize-loop-convert)
1493 {
1494 ImGui::Text("%s", std::string(pin->queue.at(i)->insTime.toYMDHMS()).c_str());
1495 }
1496 ImGui::EndChild();
1497 ImGui::TreePop();
1498 }
1499
1500 ImGui::Text("Queue blocked: %s", pin->queueBlocked ? "true" : "false");
1501 ImGui::Text("Temporal check: %s", pin->neededForTemporalQueueCheck ? "true" : "false");
1502 ImGui::Text("Drop queue: %s", pin->dropQueueIfNotFirable ? "true" : "false");
1503 ImGui::Separator();
1504 if (ImGui::MenuItem("Rename")) { renamePin = pin; }
1505 }
1506 else if (auto* pin = nm::FindOutputPin(contextPinId))
1507 {
1508 ImGui::Text("ID: %lu", size_t(pin->id));
1509 ImGui::Text("Node: %s", pin->parentNode ? std::to_string(size_t(pin->parentNode->id)).c_str() : "<none>");
1510 ImGui::Text("Type: %s", std::string(pin->type).c_str());
1511 ImGui::Text("Data available: %s", pin->noMoreDataAvailable ? "No" : "Yes");
1512 if (!pin->blocksConnectedNodeFromFinishing)
1513 {
1514 ImGui::TextUnformatted("Does not block connected pin from finishing!!!");
1515 }
1516 ImGui::Separator();
1517 if (pin->isPinLinked())
1518 {
1519 ImGui::SetNextItemOpen(true, ImGuiCond_Appearing);
1520 if (ImGui::TreeNode(fmt::format("Links: {}", pin->links.size()).c_str()))
1521 {
1522 for (const auto& link : pin->links)
1523 {
1524 ImGui::SetNextItemOpen(true, ImGuiCond_Appearing);
1525 if (ImGui::TreeNode(fmt::format("LinkId: {}", size_t(link.linkId)).c_str()))
1526 {
1527 ImGui::BulletText("Connected Node: %s", link.connectedNode->nameId().c_str());
1528 ImGui::BulletText("Connected Pin: %s (%zu)", link.getConnectedPin()->name.c_str(),
1529 size_t(link.getConnectedPin()->id));
1530 ImGui::TreePop();
1531 }
1532 }
1533 ImGui::TreePop();
1534 }
1535 }
1536 else { ImGui::TextUnformatted("Link: Not linked"); }
1537 ImGui::Separator();
1538 if (ImGui::MenuItem("Rename"))
1539 {
1540 renamePin = pin;
1541 }
1542 }
1543 else
1544 {
1545 ImGui::Text("Unknown pin: %lu", size_t(contextPinId));
1546 }
1547
1548 ImGui::EndPopup();
1549 }
1550
1551 if (renamePin) // Popup for renaming a pin
1552 {
1553 ShowRenamePinRequest(renamePin);
1554 }
1555
1556 if (ImGui::BeginPopup("Link Context Menu"))
1557 {
1558 ax::NodeEditor::PinId startPinId = 0;
1559 ax::NodeEditor::PinId endPinId = 0;
1560 for (const auto* node : nm::m_Nodes())
1561 {
1562 if (startPinId) { break; }
1563 for (const auto& output : node->outputPins)
1564 {
1565 if (startPinId) { break; }
1566 for (const auto& link : output.links)
1567 {
1568 if (link.linkId == contextLinkId)
1569 {
1570 startPinId = output.id;
1571 endPinId = link.connectedPinId;
1572 break;
1573 }
1574 }
1575 }
1576 }
1577
1578 ImGui::TextUnformatted("Link Context Menu");
1579 ImGui::Separator();
1580 if (startPinId)
1581 {
1582 ImGui::Text("ID: %lu", size_t(contextLinkId));
1583 ImGui::Text("From: %lu", size_t(startPinId));
1584 ImGui::Text("To: %lu", size_t(endPinId));
1585 }
1586 else
1587 {
1588 ImGui::Text("Unknown link: %lu", size_t(contextLinkId));
1589 }
1590 ImGui::Separator();
1591 if (ImGui::MenuItem("Delete"))
1592 {
1593 ed::DeleteLink(contextLinkId);
1594 }
1595 ImGui::EndPopup();
1596 }
1597
1598 static bool setKeyboardFocus = true;
1599 static ImVec2 newNodeSpawnPos{ -1, -1 };
1600 if (ImGui::BeginPopup("Create New Node"))
1601 {
1602 if (newNodeSpawnPos.x == -1 || newNodeSpawnPos.y == -1)
1603 {
1604 auto viewRect = reinterpret_cast<ax::NodeEditor::Detail::EditorContext*>(ed::GetCurrentEditor())->GetViewRect();
1605 newNodeSpawnPos = ImGui::GetMousePos();
1606 newNodeSpawnPos.x -= leftPaneWidth + SPLITTER_THICKNESS + 10.0F;
1607 newNodeSpawnPos.y -= menuBarHeight;
1608
1609 newNodeSpawnPos *= ed::GetCurrentZoom();
1610
1611 newNodeSpawnPos += viewRect.GetTL();
1612
1613 LOG_DEBUG("New Node will spawn at {}x{} - Zoom {}", newNodeSpawnPos.x, newNodeSpawnPos.y, ed::GetCurrentZoom());
1614 }
1615
1616 if (setKeyboardFocus)
1617 {
1618 ImGui::SetKeyboardFocusHere(0);
1619 }
1620 static ImGuiTextFilter filter;
1621
1622 filter.Draw("##NewNodeFilter");
1623
1624 if (setKeyboardFocus)
1625 {
1626 filter.Clear();
1627 setKeyboardFocus = false;
1628 }
1629
1630 Node* node = nullptr;
1631 for (const auto& [category, nodeInfoList] : NAV::NodeRegistry::RegisteredNodes())
1632 {
1633 // Prevent category from showing, if it is empty
1634 bool categoryHasItems = false;
1635 for (const auto& nodeInfo : nodeInfoList)
1636 {
1637 if (nodeInfo.hasCompatiblePin(newNodeLinkPin)
1638 && (filter.PassFilter(nodeInfo.type.c_str()) || filter.PassFilter(category.c_str())))
1639 {
1640 categoryHasItems = true;
1641 break;
1642 }
1643 }
1644 if (categoryHasItems)
1645 {
1646 ImGui::SetNextItemOpen(true, ImGuiCond_Once);
1647 if (ImGui::TreeNode((category + "##NewNodeTree").c_str()))
1648 {
1649 for (const auto& nodeInfo : nodeInfoList)
1650 {
1651 const auto& displayName = nodeInfo.type;
1652 const auto& constructor = nodeInfo.constructor;
1653 ImGui::Indent();
1654 if (nodeInfo.hasCompatiblePin(newNodeLinkPin)
1655 && (filter.PassFilter(nodeInfo.type.c_str()) || filter.PassFilter(category.c_str()))
1656 && ImGui::MenuItem(displayName.c_str()))
1657 {
1658 filter.Clear();
1659 node = constructor();
1660 nm::AddNode(node);
1661 }
1662 ImGui::Unindent();
1663 }
1664 ImGui::TreePop();
1665 }
1666 }
1667 }
1668
1669 if (node)
1670 {
1671 createNewNode = false;
1672
1673 ed::SetNodePosition(node->id, newNodeSpawnPos);
1674 newNodeSpawnPos = { -1, -1 };
1675
1676 if (auto* startPin = newNodeLinkPin)
1677 {
1678 if (startPin->kind == Pin::Kind::Input)
1679 {
1680 for (auto& pin : node->outputPins)
1681 {
1682 if (reinterpret_cast<InputPin*>(startPin)->canCreateLink(pin))
1683 {
1684 pin.createLink(*reinterpret_cast<InputPin*>(startPin));
1685 break;
1686 }
1687 }
1688 }
1689 else // if (startPin->kind == Pin::Kind::Output)
1690 {
1691 for (auto& pin : node->inputPins)
1692 {
1693 if (reinterpret_cast<OutputPin*>(startPin)->canCreateLink(pin))
1694 {
1695 reinterpret_cast<OutputPin*>(startPin)->createLink(pin);
1696 break;
1697 }
1698 }
1699 }
1700 }
1701 }
1702
1703 ImGui::EndPopup();
1704 }
1705 else
1706 {
1707 setKeyboardFocus = true;
1708 createNewNode = false;
1709 newNodeSpawnPos = { -1, -1 };
1710 }
1711 ImGui::PopStyleVar();
1712
1713 for (auto* node : nm::m_Nodes()) // Config Windows for nodes
1714 {
1715 if (node->_hasConfig && node->_showConfig)
1716 {
1717 ImVec2 center(ImGui::GetIO().DisplaySize.x * 0.5F, ImGui::GetIO().DisplaySize.y * 0.5F);
1718 if (node->_configWindowFocus)
1719 {
1720 ImGui::SetNextWindowCollapsed(false);
1721 ImGui::SetNextWindowFocus();
1722 node->_configWindowFocus = false;
1723 }
1724 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5F, 0.5F));
1725 ImGui::SetNextWindowSize(node->_guiConfigDefaultWindowSize, ImGuiCond_FirstUseEver);
1726 if (!node->_configWindowMutex.try_lock())
1727 {
1728 ImGui::SetNextWindowCollapsed(true, ImGuiCond_Always);
1729 node->_configWindowForceCollapse = true;
1730 }
1731 else
1732 {
1733 node->_configWindowMutex.unlock();
1734 if (node->_configWindowForceCollapse)
1735 {
1736 LOG_TRACE("Setting next window collapsed: {}", node->_configWindowIsCollapsed);
1737 ImGui::SetNextWindowCollapsed(node->_configWindowIsCollapsed);
1738 node->_configWindowForceCollapse = false;
1739 }
1740 }
1741 if (ImGui::Begin(fmt::format("{} ({})", node->nameId(), node->type()).c_str(), &(node->_showConfig),
1742 ImGuiWindowFlags_None))
1743 {
1744 ImGui::PushFont(WindowFont());
1745 bool locked = node->_lockConfigDuringRun && (node->callbacksEnabled || FlowExecutor::isRunning());
1746 if (locked) { ImGui::BeginDisabled(); }
1747 if (!node->_configWindowForceCollapse)
1748 {
1749 node->_configWindowIsCollapsed = false;
1750 node->guiConfig();
1751 }
1752 if (locked) { ImGui::EndDisabled(); }
1753 ImGui::PopFont();
1754 }
1755 else // Window is collapsed
1756 {
1757 if (!node->_configWindowForceCollapse) { node->_configWindowIsCollapsed = true; }
1758 if (ImGui::IsWindowFocused())
1759 {
1760 ed::EnableShortcuts(true);
1761 }
1762 }
1763
1764 ImGui::End();
1765 }
1766 }
1767 ed::Resume();
1768
1769 ed::End();
1770
1771 ImGui::PushFont(PanelFont());
1772 ImGui::Indent(SPLITTER_THICKNESS);
1773 ImGui::SetNextItemOpen(true, ImGuiCond_Once);
1774 if (ImGui::BeginTabBar("BottomViewTabBar"))
1775 {
1776 bool noItemSelected = bottomViewSelectedTab == BottomViewTabItem::None;
1777 if (noItemSelected)
1778 {
1779 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.0F);
1780 }
1781 if (ImGui::BeginTabItem("▼"))
1782 {
1783 bottomViewSelectedTab = BottomViewTabItem::None;
1784 bottomViewHeight = BOTTOM_VIEW_COLLAPSED_MIN_HEIGHT * panelFontRatio();
1785 ImGui::EndTabItem();
1786 }
1787 else
1788 {
1789 bottomViewHeight = std::max(bottomViewHeight, BOTTOM_VIEW_UNCOLLAPSED_MIN_HEIGHT);
1790 }
1791 if (noItemSelected)
1792 {
1793 ImGui::PopStyleVar();
1794 }
1795
1796 if (ImGui::BeginTabItem("Log Output", nullptr, firstFrame ? ImGuiTabItemFlags_SetSelected : ImGuiTabItemFlags_None))
1797 {
1798 static int scrollToBottom = 0;
1799 if (bottomViewSelectedTab != BottomViewTabItem::LogOutput)
1800 {
1801 scrollToBottom = 2;
1802 }
1803 bottomViewSelectedTab = BottomViewTabItem::LogOutput;
1804
1805 static bool autoScroll = true;
1806 static ImGuiTextFilter textFilter;
1807
1808 // Options menu
1809 if (ImGui::BeginPopup("Options"))
1810 {
1811 ImGui::Checkbox("Auto-scroll", &autoScroll);
1812 ImGui::EndPopup();
1813 }
1814
1815 // Main window
1816 if (ImGui::Button("Options"))
1817 {
1818 ImGui::OpenPopup("Options");
1819 }
1820 ImGui::SameLine();
1821 ImGui::SetNextItemWidth(100.0F * panelFontRatio());
1822 static int logLevelFilterSelected = spdlog::level::info;
1823 if (ImGui::BeginCombo("##LogLevelCombo", spdlog::level::to_string_view(static_cast<spdlog::level::level_enum>(logLevelFilterSelected)).begin()))
1824 {
1825 for (int n = spdlog::level::debug; n < spdlog::level::critical; n++)
1826 {
1827 const bool is_selected = (logLevelFilterSelected == n);
1828 if (ImGui::Selectable(spdlog::level::to_string_view(static_cast<spdlog::level::level_enum>(n)).begin(), is_selected))
1829 {
1830 logLevelFilterSelected = n;
1831 }
1832
1833 // Set the initial focus when opening the combo (scrolling + keyboard navigation focus)
1834 if (is_selected)
1835 {
1836 ImGui::SetItemDefaultFocus();
1837 }
1838 }
1839 ImGui::EndCombo();
1840 }
1841 ImGui::SameLine();
1842 textFilter.Draw("Filter", -100.0F);
1843
1844 ImGui::Separator();
1845 ImGui::BeginChild("scrolling", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
1846 {
1847 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
1848 ImGui::PushFont(MonoFont());
1849
1850 auto logMessages = Logger::GetRingBufferSink()->last_formatted();
1851
1852 for (auto& logLine : logMessages)
1853 {
1854 for (int n = logLevelFilterSelected; n < spdlog::level::n_levels; n++)
1855 {
1856 if (logLine.find(fmt::format("] [{}]", spdlog::level::to_short_c_str(static_cast<spdlog::level::level_enum>(n)))) != std::string::npos)
1857 {
1858 if (!textFilter.IsActive() || textFilter.PassFilter(logLine.c_str()))
1859 {
1860 // str::replace(logLine, "[T]", "\033[30m[T]\033[0m");
1861 str::replace(logLine, "[D]", "\033[36m[D]\033[0m");
1862 str::replace(logLine, "[I]", "\033[32m[I]\033[0m");
1863 str::replace(logLine, "[W]", "\033[33m[W]\033[0m");
1864 str::replace(logLine, "[E]", "\033[31m[E]\033[0m");
1865 ImGui::TextAnsiUnformatted(logLine.c_str());
1866 // ImGui::TextAnsiUnformatted("\033[31;1;4mHello\033[0mtt");
1867 }
1868 break;
1869 }
1870 }
1871 }
1872 ImGui::PopFont();
1873 ImGui::PopStyleVar();
1874
1875 if (scrollToBottom)
1876 {
1877 ImGui::SetScrollHereY(1.0F);
1878 scrollToBottom--;
1879 }
1880 else if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
1881 {
1882 ImGui::SetScrollHereY(1.0F);
1883 }
1884 }
1885 ImGui::EndChild();
1886
1887 ImGui::EndTabItem();
1888 }
1889 ImGui::EndTabBar();
1890 }
1891 ImGui::Unindent();
1892 ImGui::PopFont();
1893
1894 ImGui::EndGroup();
1895
1896 FlowAnimation::ProcessQueue();
1897
1898 // Push the Tooltip on the stack if needed
1899 if (!tooltipText.empty()) { ImGui::SetTooltip("%s", tooltipText.c_str()); }
1900
1901 ImGui::PushFont(WindowFont());
1902 gui::windows::renderGlobalWindows(m_colors, m_colorsNames);
1903 ImGui::PopFont();
1904
1905 std::string title = (flow::HasUnsavedChanges() ? "● " : "")
1906 + (flow::GetCurrentFilename().empty() ? "" : flow::GetCurrentFilename() + " - ")
1907 + "INSTINCT";
1908 SetTitle(title.c_str());
1909 }
1910
1911 float NAV::gui::NodeEditorApplication::defaultFontRatio()
1912 {
1913 return defaultFontSize.at(isUsingBigDefaultFont() ? 1 : 0) / defaultFontSize[0];
1914 }
1915
1916 float NAV::gui::NodeEditorApplication::windowFontRatio()
1917 {
1918 return windowFontSize.at(isUsingBigWindowFont() ? 1 : 0) / windowFontSize[0];
1919 }
1920
1921 float NAV::gui::NodeEditorApplication::panelFontRatio()
1922 {
1923 return panelFontSize.at(isUsingBigPanelFont() ? 1 : 0) / panelFontSize[0];
1924 }
1925
1926 float NAV::gui::NodeEditorApplication::monoFontRatio()
1927 {
1928 return monoFontSize.at(isUsingBigMonoFont() ? 1 : 0) / monoFontSize[0];
1929 }
1930
1931 float NAV::gui::NodeEditorApplication::headerFontRatio()
1932 {
1933 return headerFontSize.at(isUsingBigHeaderFont() ? 1 : 0) / headerFontSize[0];
1934 }
1935