INSTINCT Code Coverage Report


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