Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
SwitchCaseEditorModal.cpp
Go to the documentation of this file.
1/**
2 * @file SwitchCaseEditorModal.cpp
3 * @brief Implementation of SwitchCaseEditorModal for editing Switch node cases.
4 * @author Olympe Engine
5 * @date 2026-03-20
6 */
7
9#include "../../third_party/imgui/imgui.h"
10#include <algorithm>
11
12namespace Olympe {
13
15 : m_isOpen(false)
16 , m_confirmed(false)
17{
18}
19
20void SwitchCaseEditorModal::Open(const std::vector<SwitchCaseDefinition>& currentCases,
21 const std::string& switchVarName,
22 const std::string& switchVarType,
23 const std::string& currentVarValue)
24{
26 m_confirmed = false;
27 m_isOpen = true;
29
30 // Phase 26-A: Store context for rendering
34
35 // Sync buffers: one value and label per case
36 m_caseValueBuffers.clear();
37 m_caseLabelBuffers.clear();
38 for (const auto& casedef : m_editingCases)
39 {
40 m_caseValueBuffers.push_back(casedef.value);
41 m_caseLabelBuffers.push_back(casedef.customLabel);
42 }
43
44 ImGui::OpenPopup("Switch Case Editor##modal");
45}
46
48{
49 m_isOpen = false;
50 m_confirmed = false;
51 m_editingCases.clear();
52 m_caseValueBuffers.clear();
53 m_caseLabelBuffers.clear();
54}
55
57{
58 if (!m_isOpen)
59 return;
60
61 // Center the modal on screen with reasonable size
62 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
63 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
64 // Increased size: 1000px wide to accommodate all columns properly, 500px tall for scrolling
65 ImGui::SetNextWindowSize(ImVec2(1000.0f, 500.0f), ImGuiCond_Appearing);
66 ImGui::SetNextWindowSizeConstraints(ImVec2(800.0f, 300.0f), ImVec2(1200.0f, 700.0f));
67
68 bool open = true;
69 // FIXED: Removed ImGuiWindowFlags_AlwaysAutoResize to prevent animation
70 if (ImGui::BeginPopupModal("Switch Case Editor##modal", &open, ImGuiWindowFlags_NoMove))
71 {
72 ImGui::Text("Edit Switch Cases");
73 ImGui::Separator();
74
75 // Scrollable case list
77
78 ImGui::Separator();
79
80 // Action buttons
82
83 ImGui::EndPopup();
84 }
85
86 if (!open)
87 {
88 Close();
89 }
90}
91
93{
94 // Phase 26-A: Display context header
95 if (!m_switchVarName.empty())
96 {
97 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.8f, 1.0f, 1.0f)); // Cyan
98 ImGui::Text("Switching on: %s", m_switchVarName.c_str());
99 ImGui::PopStyleColor();
100
101 if (!m_switchVarType.empty())
102 {
103 ImGui::SameLine();
104 ImGui::TextDisabled("(%s)", m_switchVarType.c_str());
105 }
106
107 if (!m_currentVarValue.empty())
108 {
109 ImGui::TextDisabled("Current Value: %s", m_currentVarValue.c_str());
110 }
111
112 ImGui::TextDisabled("Add cases for each value to match below.");
113 ImGui::Separator();
114 }
115
116 // Ensure buffers match the case count
117 while (m_caseValueBuffers.size() < m_editingCases.size())
118 {
119 m_caseValueBuffers.push_back("");
120 m_caseLabelBuffers.push_back("");
121 }
122 while (m_caseValueBuffers.size() > m_editingCases.size())
123 {
124 m_caseValueBuffers.pop_back();
125 m_caseLabelBuffers.pop_back();
126 }
127
128 // Phase 26-A Fix: Fixed header + scrollable content
129 // Draw the header OUTSIDE the scroll area so it stays fixed
130 ImGui::Columns(5, "CaseListColumns", false);
131 ImGui::SetColumnWidth(0, 35.0f); // Index
132 ImGui::SetColumnWidth(1, 140.0f); // Match Value (INCREASED)
133 ImGui::SetColumnWidth(2, 180.0f); // Display Name (INCREASED)
134 ImGui::SetColumnWidth(3, 120.0f); // Pin Name (INCREASED)
135 ImGui::SetColumnWidth(4, 110.0f); // Actions (INCREASED)
136
137 ImGui::TextDisabled("#");
138 ImGui::NextColumn();
139 ImGui::TextDisabled("âš™ī¸ Match Value");
140 ImGui::NextColumn();
141 ImGui::TextDisabled("đŸ‘ī¸ Display Name");
142 ImGui::NextColumn();
143 ImGui::TextDisabled("Pin Name");
144 ImGui::NextColumn();
145 ImGui::TextDisabled("Actions");
146 ImGui::NextColumn();
147 ImGui::Separator();
148
149 ImGui::Columns(1); // Reset columns before scrollable area
150
151 // NOW the scrollable area
152 ImGui::BeginChild("CaseListScroll##modal", ImVec2(0, 300), true);
153
154 // Reset validation error flag before checking
155 m_hasValidationError = false;
156
157 // Draw rows with columns INSIDE scroll area
158 ImGui::Columns(5, "CaseListColumns", false);
159 ImGui::SetColumnWidth(0, 35.0f); // Index (same widths as header)
160 ImGui::SetColumnWidth(1, 140.0f); // Match Value
161 ImGui::SetColumnWidth(2, 180.0f); // Display Name
162 ImGui::SetColumnWidth(3, 120.0f); // Pin Name
163 ImGui::SetColumnWidth(4, 110.0f); // Actions
164
165 for (size_t i = 0; i < m_editingCases.size(); ++i)
166 {
167 ImGui::PushID(static_cast<int>(i));
168
169 bool modified = RenderCaseRow(i);
170 if (modified)
171 {
172 // Sync back to the case
174 m_editingCases[i].customLabel = m_caseLabelBuffers[i];
175 }
176
177 ImGui::PopID();
178 }
179
180 ImGui::Columns(1);
181 ImGui::EndChild();
182}
183
185{
186 bool modified = false;
187
188 // Index column
189 ImGui::TextDisabled("%zu", caseIndex);
190 ImGui::NextColumn();
191
192 // Phase 26-A: Match Value column (blue background, critical field)
193 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.3f, 0.5f, 1.0f));
194 float columnWidth = ImGui::GetColumnWidth() - 35.0f; // Leave space for validation indicator
195 ImGui::SetNextItemWidth(columnWidth);
196
197 char valueBuf[256];
199 if (ImGui::InputText("##value", valueBuf, sizeof(valueBuf)))
200 {
202 modified = true;
203 }
204 ImGui::PopStyleColor();
205
206 // Phase 26-A: Validation check for duplicate or empty value
207 bool hasError = false;
208 ImGui::SameLine();
210 {
211 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "âš ī¸");
212 if (ImGui::IsItemHovered())
213 ImGui::SetTooltip("Match value cannot be empty");
214 hasError = true;
215 }
216 else
217 {
218 // Check for duplicate
219 bool isDuplicate = false;
220 for (size_t j = 0; j < m_editingCases.size(); ++j)
221 {
223 {
224 isDuplicate = true;
225 break;
226 }
227 }
228 if (isDuplicate)
229 {
230 ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "❌");
231 if (ImGui::IsItemHovered())
232 ImGui::SetTooltip("Duplicate value - another case already uses this");
233 hasError = true;
235 }
236 else
237 {
238 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "✓");
239 }
240 }
241
242 if (hasError)
244
245 ImGui::NextColumn();
246
247 // Phase 26-A: Display Label column (green background, optional field)
248 ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.4f, 0.2f, 1.0f));
249 columnWidth = ImGui::GetColumnWidth() - 20.0f; // Leave space for info indicator
250 ImGui::SetNextItemWidth(columnWidth);
251
252 char labelBuf[256];
254 if (ImGui::InputText("##label", labelBuf, sizeof(labelBuf)))
255 {
257 modified = true;
258 }
259 ImGui::PopStyleColor();
260
261 // Info indicator for empty label
262 ImGui::SameLine();
264 {
265 ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "â„šī¸");
266 if (ImGui::IsItemHovered())
267 ImGui::SetTooltip("Display name is optional - will show pin name on canvas");
268 }
269
270 ImGui::NextColumn();
271
272 // Phase 26-A: Pin Name column (read-only, with copy button)
273 std::string pinName = m_editingCases[caseIndex].pinName;
274 ImGui::TextDisabled("%s", pinName.c_str());
275 ImGui::SameLine();
276 if (ImGui::SmallButton("Copy##pin"))
277 {
278 ImGui::SetClipboardText(pinName.c_str());
279 }
280 if (ImGui::IsItemHovered())
281 ImGui::SetTooltip("Copy pin name to clipboard");
282
283 ImGui::NextColumn();
284
285 // Action buttons (Move Up, Move Down, Delete) with tooltips
286 if (caseIndex > 0 && ImGui::Button("^##up", ImVec2(25, 0)))
287 {
291 modified = true;
292 }
293 if (ImGui::IsItemHovered())
294 ImGui::SetTooltip("Move this case up");
295
296 ImGui::SameLine();
297 if (caseIndex < m_editingCases.size() - 1 && ImGui::Button("v##down", ImVec2(25, 0)))
298 {
302 modified = true;
303 }
304 if (ImGui::IsItemHovered())
305 ImGui::SetTooltip("Move this case down");
306
307 ImGui::SameLine();
308 if (ImGui::Button("X##delete", ImVec2(25, 0)))
309 {
310 m_editingCases.erase(m_editingCases.begin() + caseIndex);
313 modified = true;
314 }
315 if (ImGui::IsItemHovered())
316 ImGui::SetTooltip("Delete this case");
317
318 ImGui::NextColumn();
319
320 return modified;
321}
322
324{
325 // Phase 26-A: "Add Case" button with intelligent defaults
326 if (ImGui::Button("Add Case", ImVec2(100, 0)))
327 {
329
330 // Intelligent suggestion: if all cases are numeric, suggest next number
331 bool allNumeric = true;
332 int maxNum = -1;
333 for (const auto& caseData : m_editingCases)
334 {
335 try
336 {
337 int num = std::stoi(caseData.value);
338 if (num > maxNum) maxNum = num;
339 }
340 catch (...)
341 {
342 allNumeric = false;
343 break;
344 }
345 }
346
347 if (allNumeric && maxNum >= 0)
348 {
349 // Suggest next sequential number
350 newCase.value = std::to_string(maxNum + 1);
351 newCase.customLabel = "State_" + std::to_string(maxNum + 1);
352 }
353 else
354 {
355 // Empty for user to fill
356 newCase.value = "";
357 newCase.customLabel = "";
358 }
359
360 newCase.pinName = "Case_" + std::to_string(m_editingCases.size());
361 m_editingCases.push_back(newCase);
362 m_caseValueBuffers.push_back(newCase.value);
363 m_caseLabelBuffers.push_back(newCase.customLabel);
364
365 // Auto-focus the new value field
366 ImGui::SetKeyboardFocusHere(-1);
367 }
368
369 ImGui::SameLine();
370 ImGui::Spacing();
371
372 // Phase 26-A: Show validation error message if any
374 {
375 ImGui::SameLine();
376 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "âš ī¸ Fix errors before applying");
377 }
378
379 ImGui::SameLine(ImGui::GetWindowWidth() - 210);
380
381 // Phase 26-A: "Apply" button disabled if validation errors exist
383 {
384 ImGui::BeginDisabled();
385 }
386 if (ImGui::Button("Apply", ImVec2(100, 0)))
387 {
388 m_confirmed = true;
389 ImGui::CloseCurrentPopup();
390 m_isOpen = false;
391 }
393 {
394 ImGui::EndDisabled();
395 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
396 ImGui::SetTooltip("Cannot apply - please fix validation errors (marked with ❌)");
397 }
398
399 ImGui::SameLine();
400
401 // "Cancel" button
402 if (ImGui::Button("Cancel", ImVec2(100, 0)))
403 {
404 m_confirmed = false;
405 ImGui::CloseCurrentPopup();
406 m_isOpen = false;
407 }
408}
409
410} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Dedicated modal dialog for editing a Switch node's case definitions (Phase 26).
std::string m_currentVarValue
Current value for context display.
std::vector< SwitchCaseDefinition > m_editingCases
Working copy of switch cases being edited.
void RenderCaseList()
Renders the scrollable list of case rows.
bool m_confirmed
True if the user clicked "Apply" (set to false on Close)
std::vector< std::string > m_caseLabelBuffers
One per case, synced before render.
SwitchCaseEditorModal()
Constructs the modal.
std::vector< std::string > m_caseValueBuffers
Temporary edit buffers for value and label fields.
std::string m_switchVarName
Variable being switched (e.g. "mHealth")
void RenderActionButtons()
Renders the "Add Case" and action buttons (Apply / Cancel).
bool m_hasValidationError
Set to true if any case has errors (prevents Apply)
std::string m_switchVarType
Variable type (e.g. "Int", "String")
void Open(const std::vector< SwitchCaseDefinition > &currentCases, const std::string &switchVarName="", const std::string &switchVarType="", const std::string &currentVarValue="")
Opens the modal, loading the given switch cases for editing.
void Close()
Closes the modal without confirming changes.
void Render()
Renders the modal dialog.
bool m_isOpen
True if the modal is currently open.
bool RenderCaseRow(size_t caseIndex)
Renders a single case row with all controls.
< Provides AssetID and INVALID_ASSET_ID
Describes a single case branch on a Switch node.