INSTINCT Code Coverage Report


Directory: src/
File: internal/gui/NodeEditorApplication.cpp
Date: 2025-07-19 10:51:51
Exec Total Coverage
Lines: 0 1031 0.0%
Functions: 0 19 0.0%
Branches: 0 2448 0.0%

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