INSTINCT Code Coverage Report


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