yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
feature_flag_editor_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cstring>
6#include <filesystem>
7#include <fstream>
8#include <sstream>
9
10#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
13#include "core/project.h"
14#include "imgui/imgui.h"
15
16namespace yaze {
17namespace editor {
18
19namespace {
20
21// Status indicator colors
22constexpr ImVec4 kColorEnabled(0.2f, 0.8f, 0.2f, 1.0f); // Green
23constexpr ImVec4 kColorDisabled(0.8f, 0.2f, 0.2f, 1.0f); // Red
24constexpr ImVec4 kColorDirty(0.9f, 0.7f, 0.1f, 1.0f); // Yellow
25constexpr ImVec4 kColorInfo(0.6f, 0.6f, 0.6f, 1.0f); // Gray
26
27// Header comment block for the generated file
28constexpr const char* kFileHeader =
29 "; Feature override flags (for isolation testing).\n"
30 "; Set to 1 to enable a feature, 0 to disable it.\n"
31 "; This file is included after Util/macros.asm and overrides defaults.\n"
32 "; Generated by yaze Feature Flag Editor.\n\n";
33
34bool ContainsCaseInsensitive(const std::string& haystack,
35 const std::string& needle) {
36 if (needle.empty())
37 return true;
38 auto it = std::search(haystack.begin(), haystack.end(), needle.begin(),
39 needle.end(), [](char a, char b) {
40 return std::tolower(static_cast<unsigned char>(a)) ==
41 std::tolower(static_cast<unsigned char>(b));
42 });
43 return it != haystack.end();
44}
45
46} // namespace
47
50
52 if (!project_) {
53 ImGui::TextDisabled("No project loaded.");
54 ImGui::TextWrapped(
55 "Open a yaze project with a hack_manifest.json to view feature flags.");
56 return;
57 }
58
59 const auto& manifest = project_->hack_manifest;
60 if (!manifest.loaded()) {
61 ImGui::TextDisabled("No hack manifest loaded.");
62 ImGui::TextWrapped(
63 "The project does not have a hack_manifest.json, or it failed to "
64 "load. Check the project settings to configure the manifest path.");
65 return;
66 }
67
68 // Refresh local copy if needed
69 if (needs_refresh_) {
71 needs_refresh_ = false;
72 }
73
74 // Header info
75 ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), ICON_MD_FLAG " %s",
76 manifest.hack_name().c_str());
77 ImGui::SameLine();
78 ImGui::TextDisabled("(%d flags)", static_cast<int>(flags_.size()));
79
80 // Config file path
81 std::string config_path = ResolveConfigPath();
82 if (!config_path.empty()) {
83 ImGui::TextDisabled("Config: %s", config_path.c_str());
84 }
85
86 ImGui::Separator();
87
88 // Toolbar: filter + actions
89 float save_width = ImGui::CalcTextSize(ICON_MD_SAVE " Save").x +
90 ImGui::GetStyle().FramePadding.x * 2.0f;
91 float refresh_width = ImGui::CalcTextSize(ICON_MD_REFRESH " Refresh").x +
92 ImGui::GetStyle().FramePadding.x * 2.0f;
93 float filter_width = ImGui::GetContentRegionAvail().x - save_width -
94 refresh_width - ImGui::GetStyle().ItemSpacing.x * 2.0f;
95
96 ImGui::SetNextItemWidth(std::max(100.0f, filter_width));
97 ImGui::InputTextWithHint("##flag_filter", "Filter flags...", filter_text_,
98 IM_ARRAYSIZE(filter_text_));
99 ImGui::SameLine();
100
101 // Count dirty flags
102 int dirty_count = 0;
103 for (const auto& flag : flags_) {
104 if (flag.dirty)
105 dirty_count++;
106 }
107
108 {
109 std::optional<gui::StyleColorGuard> dirty_guard;
110 if (dirty_count > 0) {
111 dirty_guard.emplace(ImGuiCol_Button, kColorDirty);
112 }
113 if (ImGui::Button(
114 absl::StrFormat(
115 ICON_MD_SAVE " Save%s",
116 dirty_count > 0 ? absl::StrFormat(" (%d)", dirty_count) : "")
117 .c_str())) {
118 if (SaveToFile()) {
119 status_message_ = "Saved feature flags successfully.";
120 status_is_error_ = false;
121 // Mark all as clean
122 for (auto& flag : flags_) {
123 flag.dirty = false;
124 }
125 } else {
126 status_is_error_ = true;
127 // status_message_ already set by SaveToFile()
128 }
129 }
130 }
131
132 ImGui::SameLine();
133 if (ImGui::Button(ICON_MD_REFRESH " Refresh")) {
134 needs_refresh_ = true;
135 status_message_ = "Refreshed from manifest.";
136 status_is_error_ = false;
137 }
138
139 // Status message
140 if (!status_message_.empty()) {
142 ImGui::TextColored(color, "%s", status_message_.c_str());
143 }
144
145 ImGui::Spacing();
146
147 // Flags table
148 ImGuiTableFlags table_flags =
149 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
150 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable;
151
152 if (ImGui::BeginTable("FeatureFlagsTable", 5, table_flags)) {
153 ImGui::TableSetupColumn("Toggle", ImGuiTableColumnFlags_WidthFixed, 40.0f);
154 ImGui::TableSetupColumn("Flag Name", ImGuiTableColumnFlags_WidthStretch);
155 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 50.0f);
156 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 70.0f);
157 ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.4f);
158 ImGui::TableHeadersRow();
159
160 std::string filter(filter_text_);
161
162 for (int i = 0; i < static_cast<int>(flags_.size()); ++i) {
163 auto& flag = flags_[i];
164
165 // Apply filter
166 if (!filter.empty() && !ContainsCaseInsensitive(flag.name, filter) &&
167 !ContainsCaseInsensitive(flag.source, filter)) {
168 continue;
169 }
170
171 ImGui::PushID(i);
172 ImGui::TableNextRow();
173
174 // Toggle checkbox
175 ImGui::TableSetColumnIndex(0);
176 bool enabled = flag.enabled;
177 if (ImGui::Checkbox("##toggle", &enabled)) {
178 flag.enabled = enabled;
179 flag.value = enabled ? 1 : 0;
180 flag.dirty = true;
181 }
182
183 // Flag name
184 ImGui::TableSetColumnIndex(1);
185 if (flag.dirty) {
186 ImGui::TextColored(kColorDirty, "%s", flag.name.c_str());
187 } else {
188 ImGui::TextUnformatted(flag.name.c_str());
189 }
190
191 // Value
192 ImGui::TableSetColumnIndex(2);
193 ImGui::Text("%d", flag.value);
194
195 // Status badge
196 ImGui::TableSetColumnIndex(3);
197 if (flag.enabled) {
198 ImGui::TextColored(kColorEnabled, "ON");
199 } else {
200 ImGui::TextColored(kColorDisabled, "OFF");
201 }
202 if (flag.dirty) {
203 ImGui::SameLine();
204 ImGui::TextColored(kColorDirty, "*");
205 }
206
207 // Source
208 ImGui::TableSetColumnIndex(4);
209 ImGui::TextColored(kColorInfo, "%s", flag.source.c_str());
210
211 ImGui::PopID();
212 }
213
214 ImGui::EndTable();
215 }
216
217 // Summary
218 int enabled_count = 0;
219 for (const auto& flag : flags_) {
220 if (flag.enabled)
221 enabled_count++;
222 }
223 ImGui::Separator();
224 ImGui::TextDisabled("%d/%d flags enabled", enabled_count,
225 static_cast<int>(flags_.size()));
226 if (dirty_count > 0) {
227 ImGui::SameLine();
228 ImGui::TextColored(kColorDirty, "(%d unsaved changes)", dirty_count);
229 }
230}
231
233 flags_.clear();
234
236 return;
237 }
238
239 const auto& manifest_flags = project_->hack_manifest.feature_flags();
240 flags_.reserve(manifest_flags.size());
241
242 for (const auto& mf : manifest_flags) {
243 EditableFlag ef;
244 ef.name = mf.name;
245 ef.value = mf.value;
246 ef.enabled = mf.enabled;
247 ef.source = mf.source;
248 ef.dirty = false;
249 flags_.push_back(std::move(ef));
250 }
251}
252
254 if (!project_)
255 return "";
256
257 // Use code_folder from the project to find Config/feature_flags.asm
258 if (project_->code_folder.empty())
259 return "";
260
261 std::string code_path = project_->GetAbsolutePath(project_->code_folder);
262 auto candidate =
263 std::filesystem::path(code_path) / "Config" / "feature_flags.asm";
264 return candidate.string();
265}
266
268 std::string config_path = ResolveConfigPath();
269 if (config_path.empty()) {
270 status_message_ = "Cannot determine config file path (no code folder set).";
271 return false;
272 }
273
274 // Ensure the directory exists
275 auto parent = std::filesystem::path(config_path).parent_path();
276 if (!std::filesystem::exists(parent)) {
278 absl::StrFormat("Config directory does not exist: %s", parent.string());
279 return false;
280 }
281
282 // Write to a temporary file, then rename for atomicity
283 std::string tmp_path = config_path + ".tmp";
284 {
285 std::ofstream out(tmp_path);
286 if (!out.is_open()) {
288 absl::StrFormat("Failed to open temp file: %s", tmp_path);
289 return false;
290 }
291
292 out << kFileHeader;
293
294 // Find the longest flag name for alignment
295 size_t max_name_len = 0;
296 for (const auto& flag : flags_) {
297 max_name_len = std::max(max_name_len, flag.name.size());
298 }
299
300 // Write each flag, aligned
301 for (const auto& flag : flags_) {
302 // Pad the flag name for visual alignment
303 std::string padded_name = flag.name;
304 while (padded_name.size() < max_name_len) {
305 padded_name += ' ';
306 }
307 out << padded_name << " = " << flag.value << "\n";
308 }
309 }
310
311 // Atomic rename
312 std::error_code ec;
313 std::filesystem::rename(tmp_path, config_path, ec);
314 if (ec) {
316 absl::StrFormat("Failed to rename temp file: %s", ec.message());
317 // Clean up temp file
318 std::filesystem::remove(tmp_path, ec);
319 return false;
320 }
321
322 return true;
323}
324
325} // namespace editor
326} // namespace yaze
const std::vector< FeatureFlag > & feature_flags() const
bool loaded() const
Check if the manifest has been loaded.
void RefreshFromManifest()
Refresh the local flags list from the hack manifest.
bool SaveToFile()
Write the current flag state to Config/feature_flags.asm.
void Draw()
Draw the panel content (no ImGui::Begin/End).
std::string ResolveConfigPath() const
Resolve the absolute path to Config/feature_flags.asm.
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_FLAG
Definition icons.h:784
#define ICON_MD_SAVE
Definition icons.h:1644
constexpr ImVec4 kColorDisabled(0.8f, 0.2f, 0.2f, 1.0f)
constexpr ImVec4 kColorEnabled(0.2f, 0.8f, 0.2f, 1.0f)
bool ContainsCaseInsensitive(const std::string &haystack, const std::string &needle)
constexpr ImVec4 kColorDirty(0.9f, 0.7f, 0.1f, 1.0f)
constexpr ImVec4 kColorInfo(0.6f, 0.6f, 0.6f, 1.0f)
core::HackManifest hack_manifest
Definition project.h:204
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1319