Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BTNodePropertyPanel.cpp
Go to the documentation of this file.
1/**
2 * @file BTNodePropertyPanel.cpp
3 * @brief Property editor panel implementation for BehaviorTree nodes
4 * @author Olympe Engine
5 * @date 2026-04-08
6 */
7
10#include "../third_party/imgui/imgui.h"
11#include "../DataManager.h"
12#include "../AI/BehaviorTree.h"
13#include "../system/system_utils.h"
14#include "../Editor/Modals/FilePickerModal.h"
15#include <cstring>
16
17namespace Olympe {
18
20 : m_activeGraphId(-1)
21 , m_selectedNodeId(-1)
22{
23 std::memset(m_nodeNameBuffer, 0, sizeof(m_nodeNameBuffer));
24 std::memset(m_paramBuffer, 0, sizeof(m_paramBuffer));
25}
26
28{
29 // Nothing to initialize currently
30}
31
32void BTNodePropertyPanel::SetSelectedNode(int graphId, int nodeId)
33{
34 m_activeGraphId = graphId;
35 m_selectedNodeId = nodeId;
36
37 if (graphId >= 0 && nodeId >= 0)
38 {
40 if (graph)
41 {
42 const GraphNode* node = graph->GetNode(nodeId);
43 if (node)
44 {
46 }
47 }
48 }
49}
50
52{
53 m_activeGraphId = -1;
55 std::memset(m_nodeNameBuffer, 0, sizeof(m_nodeNameBuffer));
56 std::memset(m_paramBuffer, 0, sizeof(m_paramBuffer));
57}
58
60{
61 if (m_activeGraphId < 0 || m_selectedNodeId < 0)
62 {
63 ImGui::TextDisabled("No node selected");
64 return;
65 }
66
68 if (!graph)
69 {
70 ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: Graph not found");
72 return;
73 }
74
76 if (!node)
77 {
78 ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: Node not found");
80 return;
81 }
82
83 ImGui::Text("Node ID: %d", node->id);
84 ImGui::Separator();
85
87 ImGui::Separator();
88
89 // Phase 39c: Check if this is a SubGraph node
90 if (node->type == NodeType::BT_SubGraph)
91 {
93 ImGui::Separator();
94 }
95
97}
98
100{
101 ImGui::Text("Node Type: %s", NodeTypeToString(node->type));
102
103 // Node name editing
104 if (ImGui::InputText("##NodeName", m_nodeNameBuffer, sizeof(m_nodeNameBuffer)))
105 {
106 // Will apply on next ApplyNodeChanges call
107 }
108
109 if (ImGui::IsItemDeactivatedAfterEdit())
110 {
112 if (graph)
113 {
115 if (mutableNode)
116 {
118 SYSTEM_LOG << "[BTNodePropertyPanel] Updated node name to: " << m_nodeNameBuffer << "\n";
119 }
120 }
121 }
122
123 // Position info (read-only for now)
124 ImGui::Text("Position: (%.1f, %.1f)", node->posX, node->posY);
125}
126
128{
129 ImGui::PushID(node->id);
130
131 // For BT nodes, parameters are stored in a generic map
132 if (node->parameters.empty())
133 {
134 ImGui::TextDisabled("No parameters");
135 ImGui::PopID();
136 return;
137 }
138
139 ImGui::TextUnformatted("Parameters:");
140 for (const auto& param : node->parameters)
141 {
142 ImGui::BulletText("%s: %s", param.first.c_str(), param.second.c_str());
143 }
144
145 ImGui::PopID();
146}
147
149{
150 if (!node)
151 return;
152
153 node->name = m_nodeNameBuffer;
154 SYSTEM_LOG << "[BTNodePropertyPanel] Applied changes to node: " << node->id << "\n";
155}
156
158{
159 if (!node)
160 return;
161
162 ImGui::TextUnformatted("SubGraph Configuration:");
163 ImGui::Indent();
164
165 // Display SubGraph path with file browser button
166 auto pathIt = node->parameters.find("subgraphPath");
167 std::string currentPath = (pathIt != node->parameters.end()) ? pathIt->second : "";
168
169 if (!currentPath.empty())
170 {
171 ImGui::TextWrapped("Path: %s", currentPath.c_str());
172 ImGui::SameLine();
173 ImGui::TextColored(ImVec4(0, 1, 0, 1), "✓");
174 }
175 else
176 {
177 ImGui::TextDisabled("(No SubGraph path specified)");
178 }
179
180 // File browser button - Phase 40: Using centralized file picker modal
181 if (ImGui::Button("Browse...##subgraphPath", ImVec2(-1, 0)))
182 {
184 }
185
186 // Phase 40: Check if modal has closed with a selection
187 // This pattern detects when modal was open last frame but is now closed
188 static bool wasFilePickerOpen = false;
190
192 {
193 // Modal just closed - check if user selected a file
195 if (!selectedFile.empty())
196 {
197 // Update the node's subgraphPath parameter with the selected file
198 node->parameters["subgraphPath"] = selectedFile;
199 SYSTEM_LOG << "[BTNodePropertyPanel] Updated SubGraph path: " << selectedFile << "\n";
200 }
201 }
203
204 // Phase 39c Step 6: Validation error display
205 ImGui::Spacing();
207
208 if (!validationErrors.empty())
209 {
210 // Display error section with red highlighting
211 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); // Red
212 ImGui::TextWrapped("❌ Validation Errors:");
213 ImGui::PopStyleColor();
214
215 ImGui::Indent();
216 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f)); // Lighter red
217 for (const auto& error : validationErrors)
218 {
219 ImGui::BulletText("%s", error.c_str());
220 }
221 ImGui::PopStyleColor();
222 ImGui::Unindent();
223
224 // Add visual separator
225 ImGui::Spacing();
226 ImGui::Separator();
227 }
228 else if (!currentPath.empty())
229 {
230 // Show success message only if path is specified
231 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); // Green
232 ImGui::TextWrapped("✓ No validation errors");
233 ImGui::PopStyleColor();
234 ImGui::Spacing();
235 }
236
237 ImGui::Separator();
238
239 // Display input parameters
240 auto inputCountIt = node->parameters.find("inputParamCount");
241 int inputCount = 0;
242 if (inputCountIt != node->parameters.end())
243 {
244 try
245 {
246 inputCount = std::stoi(inputCountIt->second);
247 }
248 catch (const std::exception&)
249 {
250 inputCount = 0;
251 }
252 }
253
254 if (inputCount > 0)
255 {
256 ImGui::TextUnformatted("Input Parameters:");
257 ImGui::Indent();
258 for (int i = 0; i < inputCount; ++i)
259 {
260 char keyBuf[64];
261 snprintf(keyBuf, sizeof(keyBuf), "inputParam_%d", i);
262 auto it = node->parameters.find(keyBuf);
263 if (it != node->parameters.end())
264 {
265 ImGui::BulletText("%s", it->second.c_str());
266 }
267 }
268 ImGui::Unindent();
269 }
270 else
271 {
272 ImGui::TextDisabled("(No input parameters)");
273 }
274
275 ImGui::Separator();
276
277 // Display output parameters
278 auto outputCountIt = node->parameters.find("outputParamCount");
279 int outputCount = 0;
280 if (outputCountIt != node->parameters.end())
281 {
282 try
283 {
284 outputCount = std::stoi(outputCountIt->second);
285 }
286 catch (const std::exception&)
287 {
288 outputCount = 0;
289 }
290 }
291
292 if (outputCount > 0)
293 {
294 ImGui::TextUnformatted("Output Parameters:");
295 ImGui::Indent();
296 for (int i = 0; i < outputCount; ++i)
297 {
298 char keyBuf[64];
299 snprintf(keyBuf, sizeof(keyBuf), "outputParam_%d", i);
300 auto it = node->parameters.find(keyBuf);
301 if (it != node->parameters.end())
302 {
303 ImGui::BulletText("%s", it->second.c_str());
304 }
305 }
306 ImGui::Unindent();
307 }
308 else
309 {
310 ImGui::TextDisabled("(No output parameters)");
311 }
312
313 ImGui::Unindent();
314 ImGui::Separator();
315
316 // Phase 39c Step 4: Parameter binding editor
318}
319
321{
322 if (!node)
323 return;
324
325 ImGui::TextUnformatted("Parameter Bindings:");
326 ImGui::Indent();
327
328 // ===== INPUT BINDINGS =====
329 ImGui::TextUnformatted("Input Bindings (Parent → Child):");
330 ImGui::Indent();
331
332 // Extract input binding count
333 auto inputBindingCountIt = node->parameters.find("inputBindingCount");
334 int inputBindingCount = 0;
335 if (inputBindingCountIt != node->parameters.end())
336 {
337 try
338 {
339 inputBindingCount = std::stoi(inputBindingCountIt->second);
340 }
341 catch (const std::exception&)
342 {
344 }
345 }
346
347 // Display existing input bindings in table format
350 {
351 ImGui::BeginTable("##inputBindings", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg);
352 ImGui::TableSetupColumn("Child Param", ImGuiTableColumnFlags_WidthStretch);
353 ImGui::TableSetupColumn("Parent Param", ImGuiTableColumnFlags_WidthStretch);
354 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 60.0f);
355 ImGui::TableHeadersRow();
356
357 for (int i = 0; i < inputBindingCount; ++i)
358 {
359 ImGui::TableNextRow();
360
361 // Child param name (column 0)
362 char childKeyBuf[64];
363 snprintf(childKeyBuf, sizeof(childKeyBuf), "inputBinding_%d_child", i);
364 auto childIt = node->parameters.find(childKeyBuf);
365 std::string childParam = (childIt != node->parameters.end()) ? childIt->second : "";
366
367 ImGui::TableSetColumnIndex(0);
368 ImGui::TextDisabled("%s", childParam.c_str());
369
370 // Parent param name (column 1) - editable
371 char parentKeyBuf[64];
372 snprintf(parentKeyBuf, sizeof(parentKeyBuf), "inputBinding_%d_parent", i);
373 auto parentIt = node->parameters.find(parentKeyBuf);
374 std::string currentParentParam = (parentIt != node->parameters.end()) ? parentIt->second : "";
375
376 ImGui::TableSetColumnIndex(1);
377 char parentBuf[256];
379 ImGui::SetNextItemWidth(-1.0f);
380 if (ImGui::InputText(("##inputBinding_" + std::to_string(i) + "_parent").c_str(), parentBuf, sizeof(parentBuf)))
381 {
382 node->parameters[parentKeyBuf] = parentBuf;
383 }
384
385 // Delete button (column 2)
386 ImGui::TableSetColumnIndex(2);
387 if (ImGui::Button(("Del##input_" + std::to_string(i)).c_str(), ImVec2(-1, 0)))
388 {
389 // Mark for deletion by shifting remaining bindings
390 for (int j = i; j < inputBindingCount - 1; ++j)
391 {
392 char srcChild[64], srcParent[64];
393 char dstChild[64], dstParent[64];
394 snprintf(srcChild, sizeof(srcChild), "inputBinding_%d_child", j + 1);
395 snprintf(srcParent, sizeof(srcParent), "inputBinding_%d_parent", j + 1);
396 snprintf(dstChild, sizeof(dstChild), "inputBinding_%d_child", j);
397 snprintf(dstParent, sizeof(dstParent), "inputBinding_%d_parent", j);
398
399 auto srcChildIt = node->parameters.find(srcChild);
400 auto srcParentIt = node->parameters.find(srcParent);
401
402 if (srcChildIt != node->parameters.end())
403 node->parameters[dstChild] = srcChildIt->second;
404 if (srcParentIt != node->parameters.end())
405 node->parameters[dstParent] = srcParentIt->second;
406 }
408 node->parameters["inputBindingCount"] = std::to_string(inputBindingCount);
409
410 // Clean up last binding
411 char lastChild[64], lastParent[64];
412 snprintf(lastChild, sizeof(lastChild), "inputBinding_%d_child", inputBindingCount);
413 snprintf(lastParent, sizeof(lastParent), "inputBinding_%d_parent", inputBindingCount);
414 node->parameters.erase(lastChild);
415 node->parameters.erase(lastParent);
416 }
417 }
418 ImGui::EndTable();
419 }
420 else
421 {
422 ImGui::TextDisabled("(No input bindings)");
423 }
424
425 // Add input binding button
426 if (ImGui::Button("+ Add Input Binding##input", ImVec2(-1, 0)))
427 {
428 char childKeyBuf[64], parentKeyBuf[64];
429 snprintf(childKeyBuf, sizeof(childKeyBuf), "inputBinding_%d_child", inputBindingCount);
430 snprintf(parentKeyBuf, sizeof(parentKeyBuf), "inputBinding_%d_parent", inputBindingCount);
431 node->parameters[childKeyBuf] = "newParam";
432 node->parameters[parentKeyBuf] = "";
434 node->parameters["inputBindingCount"] = std::to_string(inputBindingCount);
435 }
436
437 ImGui::Unindent();
438 ImGui::Separator();
439
440 // ===== OUTPUT BINDINGS =====
441 ImGui::TextUnformatted("Output Bindings (Child → Parent):");
442 ImGui::Indent();
443
444 // Extract output binding count
445 auto outputBindingCountIt = node->parameters.find("outputBindingCount");
446 int outputBindingCount = 0;
447 if (outputBindingCountIt != node->parameters.end())
448 {
449 try
450 {
451 outputBindingCount = std::stoi(outputBindingCountIt->second);
452 }
453 catch (const std::exception&)
454 {
456 }
457 }
458
459 // Display existing output bindings in table format
462 {
463 ImGui::BeginTable("##outputBindings", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg);
464 ImGui::TableSetupColumn("Parent Param", ImGuiTableColumnFlags_WidthStretch);
465 ImGui::TableSetupColumn("Child Param", ImGuiTableColumnFlags_WidthStretch);
466 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 60.0f);
467 ImGui::TableHeadersRow();
468
469 for (int i = 0; i < outputBindingCount; ++i)
470 {
471 ImGui::TableNextRow();
472
473 // Parent param name (column 0) - editable
474 char parentKeyBuf[64];
475 snprintf(parentKeyBuf, sizeof(parentKeyBuf), "outputBinding_%d_parent", i);
476 auto parentIt = node->parameters.find(parentKeyBuf);
477 std::string currentParentParam = (parentIt != node->parameters.end()) ? parentIt->second : "";
478
479 ImGui::TableSetColumnIndex(0);
480 char parentBuf[256];
482 ImGui::SetNextItemWidth(-1.0f);
483 if (ImGui::InputText(("##outputBinding_" + std::to_string(i) + "_parent").c_str(), parentBuf, sizeof(parentBuf)))
484 {
485 node->parameters[parentKeyBuf] = parentBuf;
486 }
487
488 // Child param name (column 1)
489 char childKeyBuf[64];
490 snprintf(childKeyBuf, sizeof(childKeyBuf), "outputBinding_%d_child", i);
491 auto childIt = node->parameters.find(childKeyBuf);
492 std::string childParam = (childIt != node->parameters.end()) ? childIt->second : "";
493
494 ImGui::TableSetColumnIndex(1);
495 ImGui::TextDisabled("%s", childParam.c_str());
496
497 // Delete button (column 2)
498 ImGui::TableSetColumnIndex(2);
499 if (ImGui::Button(("Del##output_" + std::to_string(i)).c_str(), ImVec2(-1, 0)))
500 {
501 // Mark for deletion by shifting remaining bindings
502 for (int j = i; j < outputBindingCount - 1; ++j)
503 {
504 char srcParent[64], srcChild[64];
505 char dstParent[64], dstChild[64];
506 snprintf(srcParent, sizeof(srcParent), "outputBinding_%d_parent", j + 1);
507 snprintf(srcChild, sizeof(srcChild), "outputBinding_%d_child", j + 1);
508 snprintf(dstParent, sizeof(dstParent), "outputBinding_%d_parent", j);
509 snprintf(dstChild, sizeof(dstChild), "outputBinding_%d_child", j);
510
511 auto srcParentIt = node->parameters.find(srcParent);
512 auto srcChildIt = node->parameters.find(srcChild);
513
514 if (srcParentIt != node->parameters.end())
515 node->parameters[dstParent] = srcParentIt->second;
516 if (srcChildIt != node->parameters.end())
517 node->parameters[dstChild] = srcChildIt->second;
518 }
520 node->parameters["outputBindingCount"] = std::to_string(outputBindingCount);
521
522 // Clean up last binding
523 char lastParent[64], lastChild[64];
524 snprintf(lastParent, sizeof(lastParent), "outputBinding_%d_parent", outputBindingCount);
525 snprintf(lastChild, sizeof(lastChild), "outputBinding_%d_child", outputBindingCount);
526 node->parameters.erase(lastParent);
527 node->parameters.erase(lastChild);
528 }
529 }
530 ImGui::EndTable();
531 }
532 else
533 {
534 ImGui::TextDisabled("(No output bindings)");
535 }
536
537 // Add output binding button
538 if (ImGui::Button("+ Add Output Binding##output", ImVec2(-1, 0)))
539 {
540 char parentKeyBuf[64], childKeyBuf[64];
541 snprintf(parentKeyBuf, sizeof(parentKeyBuf), "outputBinding_%d_parent", outputBindingCount);
542 snprintf(childKeyBuf, sizeof(childKeyBuf), "outputBinding_%d_child", outputBindingCount);
543 node->parameters[parentKeyBuf] = "";
544 node->parameters[childKeyBuf] = "newParam";
546 node->parameters["outputBindingCount"] = std::to_string(outputBindingCount);
547 }
548
549 ImGui::Unindent();
550}
551
552} // namespace Olympe
Property editor panel for BehaviorTree nodes.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static BehaviorTreeManager & Get()
std::vector< std::string > GetValidationErrors(uint32_t graphId)
static DataManager & Get()
Definition DataManager.h:91
std::string GetSelectedFileFromModal() const
Retrieves the selected file from the file picker modal.
bool IsFilePickerModalOpen() const
Checks if the file picker modal is currently visible.
std::string OpenFilePickerModal(Olympe::FilePickerType fileType, const std::string &currentPath="")
Opens a centralized file picker modal for the specified file type.
void Render()
Render the property panel.
void RenderNodeBasicInfo(const GraphNode *node)
void ClearSelection()
Clear the current selection.
void ApplyNodeChanges(GraphNode *node)
void SetSelectedNode(int graphId, int nodeId)
Set the currently selected node ID.
void RenderSubGraphBindingEditor(GraphNode *node)
Phase 39c Step 4: Parameter binding editor.
char m_nodeNameBuffer[256]
Buffer for node name editing.
void RenderNodeParameters(const GraphNode *node)
void Initialize()
Initialize the panel.
char m_paramBuffer[512]
Buffer for parameter editing.
void RenderSubGraphControls(GraphNode *node)
Phase 39c: SubGraph node editor.
int m_activeGraphId
Current graph ID in BTNodeGraphManager.
NodeGraph * GetGraph(int graphId)
static NodeGraphManager & Get()
< Provides AssetID and INVALID_ASSET_ID
@ BT_SubGraph
Phase 8: references a subgraph by UUID (BehaviorTree)
@ BehaviorTree
.bt.json files in ./Gamedata
const char * NodeTypeToString(NodeType type)
#define SYSTEM_LOG