INSTINCT Code Coverage Report


Directory: src/
File: internal/gui/NodeEditorApplication.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 0 1031 0.0%
Functions: 0 19 0.0%
Branches: 0 2418 0.0%

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