yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
settings_panel.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cstdlib>
6#include <cstring>
7#include <filesystem>
8#include <set>
9#include <sstream>
10#include <unordered_set>
11#include <vector>
12
13#include "absl/strings/ascii.h"
14#include "absl/strings/match.h"
15#include "absl/strings/str_cat.h"
16#include "absl/strings/str_format.h"
25#include "app/gui/core/icons.h"
26#include "app/gui/core/style.h"
36#include "imgui/imgui.h"
37#include "imgui/misc/cpp/imgui_stdlib.h"
38#include "nlohmann/json.hpp"
39#include "rom/rom.h"
40#include "util/file_util.h"
41#include "util/log.h"
42#include "util/platform_paths.h"
43#include "util/rom_hash.h"
45
46namespace yaze {
47namespace editor {
48
58
59namespace {
60
62 std::string text;
63 std::string error;
64};
65
66bool ParseHexToken(const std::string& token, uint16_t* out) {
67 if (!out) {
68 return false;
69 }
70 if (token.empty()) {
71 return false;
72 }
73 std::string trimmed = token;
74 if (absl::StartsWithIgnoreCase(trimmed, "0x")) {
75 trimmed = trimmed.substr(2);
76 }
77 if (trimmed.empty()) {
78 return false;
79 }
80 char* end = nullptr;
81 unsigned long value = std::strtoul(trimmed.c_str(), &end, 16);
82 if (end == nullptr || *end != '\0') {
83 return false;
84 }
85 if (value > 0xFFFFu) {
86 return false;
87 }
88 *out = static_cast<uint16_t>(value);
89 return true;
90}
91
92bool ParseHexList(const std::string& input, std::vector<uint16_t>* out,
93 std::string* error) {
94 if (!out) {
95 return false;
96 }
97 out->clear();
98 if (error) {
99 error->clear();
100 }
101 if (input.empty()) {
102 return true;
103 }
104
105 std::string normalized = input;
106 for (char& c : normalized) {
107 if (c == ',' || c == ';') {
108 c = ' ';
109 }
110 }
111
112 std::stringstream ss(normalized);
113 std::string token;
114 std::unordered_set<uint16_t> seen;
115 while (ss >> token) {
116 auto dash = token.find('-');
117 if (dash != std::string::npos) {
118 std::string left = token.substr(0, dash);
119 std::string right = token.substr(dash + 1);
120 uint16_t start = 0;
121 uint16_t end = 0;
122 if (!ParseHexToken(left, &start) || !ParseHexToken(right, &end)) {
123 if (error) {
124 *error = absl::StrFormat("Invalid range: %s", token);
125 }
126 return false;
127 }
128 if (end < start) {
129 if (error) {
130 *error = absl::StrFormat("Range end before start: %s", token);
131 }
132 return false;
133 }
134 for (uint16_t value = start; value <= end; ++value) {
135 if (seen.insert(value).second) {
136 out->push_back(value);
137 }
138 if (value == 0xFFFF) {
139 break;
140 }
141 }
142 } else {
143 uint16_t value = 0;
144 if (!ParseHexToken(token, &value)) {
145 if (error) {
146 *error = absl::StrFormat("Invalid hex value: %s", token);
147 }
148 return false;
149 }
150 if (seen.insert(value).second) {
151 out->push_back(value);
152 }
153 }
154 }
155 return true;
156}
157
158std::string FormatHexList(const std::vector<uint16_t>& values) {
159 std::string result;
160 result.reserve(values.size() * 6);
161 for (size_t i = 0; i < values.size(); ++i) {
162 const uint16_t value = values[i];
163 std::string token = value <= 0xFF ? absl::StrFormat("0x%02X", value)
164 : absl::StrFormat("0x%04X", value);
165 if (!result.empty()) {
166 result.append(", ");
167 }
168 result.append(token);
169 }
170 return result;
171}
172
173std::vector<uint16_t> DefaultTrackTiles() {
174 std::vector<uint16_t> values;
175 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
176 values.push_back(tile);
177 }
178 return values;
179}
180
181std::vector<uint16_t> DefaultStopTiles() {
182 return {0xB7, 0xB8, 0xB9, 0xBA};
183}
184
185std::vector<uint16_t> DefaultSwitchTiles() {
186 return {0xD0, 0xD1, 0xD2, 0xD3};
187}
188
189std::vector<uint16_t> DefaultTrackObjectIds() {
190 return {0x31};
191}
192
193std::vector<uint16_t> DefaultMinecartSpriteIds() {
194 return {0xA3};
195}
196
197bool IsLocalEndpoint(const std::string& base_url) {
198 if (base_url.empty()) {
199 return false;
200 }
201 std::string lower = absl::AsciiStrToLower(base_url);
202 return absl::StrContains(lower, "localhost") ||
203 absl::StrContains(lower, "127.0.0.1") ||
204 absl::StrContains(lower, "::1") ||
205 absl::StrContains(lower, "0.0.0.0") ||
206 absl::StrContains(lower, "192.168.") || absl::StartsWith(lower, "10.");
207}
208
209bool IsTailscaleEndpoint(const std::string& base_url) {
210 if (base_url.empty()) {
211 return false;
212 }
213 std::string lower = absl::AsciiStrToLower(base_url);
214 return absl::StrContains(lower, ".ts.net") ||
215 absl::StrContains(lower, "100.64.");
216}
217
219 std::vector<std::string> tags;
220 if (IsLocalEndpoint(host.base_url)) {
221 tags.push_back("local");
222 }
223 if (IsTailscaleEndpoint(host.base_url)) {
224 tags.push_back("tailscale");
225 }
226 if (absl::StartsWith(absl::AsciiStrToLower(host.base_url), "https://")) {
227 tags.push_back("https");
228 } else if (absl::StartsWith(absl::AsciiStrToLower(host.base_url),
229 "http://") &&
230 !IsLocalEndpoint(host.base_url) &&
231 !IsTailscaleEndpoint(host.base_url)) {
232 tags.push_back("http");
233 }
234 if (host.supports_vision) {
235 tags.push_back("vision");
236 }
237 if (host.supports_tools) {
238 tags.push_back("tools");
239 }
240 if (host.supports_streaming) {
241 tags.push_back("stream");
242 }
243 if (tags.empty()) {
244 return "";
245 }
246 std::string result = "[";
247 for (size_t i = 0; i < tags.size(); ++i) {
248 result += tags[i];
249 if (i + 1 < tags.size()) {
250 result += ", ";
251 }
252 }
253 result += "]";
254 return result;
255}
256
257// Expands a leading '~' or '~/' to the user's home directory so paths entered
258// by the user (or copied from other platforms) resolve on Linux/Windows.
259std::string ExpandLeadingTilde(const std::string& path) {
260 if (path.empty() || path.front() != '~') {
261 return path;
262 }
263 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
264 if (home_dir.empty() || home_dir == ".") {
265 return path;
266 }
267 if (path.size() == 1) {
268 return home_dir.string();
269 }
270 if (path[1] == '/' || path[1] == '\\') {
271 return (home_dir / path.substr(2)).string();
272 }
273 return (home_dir / path.substr(1)).string();
274}
275
276bool AddUniquePath(std::vector<std::string>* paths, const std::string& path) {
277 if (!paths || path.empty()) {
278 return false;
279 }
280 const std::string expanded = ExpandLeadingTilde(path);
281 auto it = std::find(paths->begin(), paths->end(), expanded);
282 if (it != paths->end()) {
283 return false;
284 }
285 paths->push_back(expanded);
286 return true;
287}
288
289} // namespace
290
292 if (!user_settings_) {
293 ImGui::TextDisabled(tr("Settings not available"));
294 return;
295 }
296
297 // Use collapsing headers for sections
298 // Default open the General Settings
299 if (ImGui::CollapsingHeader(ICON_MD_SETTINGS " General Settings",
300 ImGuiTreeNodeFlags_DefaultOpen)) {
301 ImGui::Indent();
303 ImGui::Unindent();
304 ImGui::Spacing();
305 }
306
307 // Add Project Settings section
308 if (ImGui::CollapsingHeader(ICON_MD_FOLDER " Project Configuration")) {
309 ImGui::Indent();
311 ImGui::Unindent();
312 ImGui::Spacing();
313 }
314
315 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " Files & Sync")) {
316 ImGui::Indent();
318 ImGui::Unindent();
319 ImGui::Spacing();
320 }
321
322 if (ImGui::CollapsingHeader(ICON_MD_PALETTE " Appearance")) {
323 ImGui::Indent();
325 ImGui::Unindent();
326 ImGui::Spacing();
327 }
328
329 if (ImGui::CollapsingHeader(ICON_MD_DASHBOARD_CUSTOMIZE
330 " Workspace Layout")) {
331 ImGui::Indent();
333 ImGui::Unindent();
334 ImGui::Spacing();
335 }
336
337 if (ImGui::CollapsingHeader(ICON_MD_TUNE " Editor Behavior")) {
338 ImGui::Indent();
340 ImGui::Unindent();
341 ImGui::Spacing();
342 }
343
344 if (ImGui::CollapsingHeader(ICON_MD_SPEED " Performance")) {
345 ImGui::Indent();
347 ImGui::Unindent();
348 ImGui::Spacing();
349 }
350
351 if (ImGui::CollapsingHeader(ICON_MD_SMART_TOY " AI Agent")) {
352 ImGui::Indent();
354 ImGui::Unindent();
355 ImGui::Spacing();
356 }
357
358 if (ImGui::CollapsingHeader(ICON_MD_KEYBOARD " Keyboard Shortcuts")) {
359 ImGui::Indent();
361 ImGui::Unindent();
362 ImGui::Spacing();
363 }
364
365 if (ImGui::CollapsingHeader(ICON_MD_EXTENSION " ASM Patches")) {
366 ImGui::Indent();
368 ImGui::Unindent();
369 }
370}
371
373 // Refactored from table to vertical list for sidebar
374 static gui::FlagsMenu flags;
375
376 ImGui::TextDisabled(tr("Feature Flags configuration"));
377 ImGui::Spacing();
378
379 if (ImGui::TreeNode(ICON_MD_FLAG " System Flags")) {
380 flags.DrawSystemFlags();
381 ImGui::TreePop();
382 }
383
384 if (ImGui::TreeNode(ICON_MD_MAP " Overworld Flags")) {
385 flags.DrawOverworldFlags();
386 ImGui::TreePop();
387 }
388
389 if (ImGui::TreeNode(ICON_MD_EXTENSION " ZSCustomOverworld Enable Flags")) {
391 ImGui::TreePop();
392 }
393
394 if (ImGui::TreeNode(ICON_MD_CASTLE " Dungeon Flags")) {
395 flags.DrawDungeonFlags();
396 ImGui::TreePop();
397 }
398
399 if (ImGui::TreeNode(ICON_MD_FOLDER_SPECIAL " Resource Flags")) {
400 flags.DrawResourceFlags();
401 ImGui::TreePop();
402 }
403}
404
406 if (!project_) {
407 ImGui::TextDisabled(tr("No active project."));
408 return;
409 }
410
411 ImGui::Text(tr("%s Project Info"), ICON_MD_INFO);
412 ImGui::Separator();
413
414 ImGui::Text(tr("Name: %s"), project_->name.c_str());
415 ImGui::Text(tr("Path: %s"), project_->filepath.c_str());
416
417 ImGui::Spacing();
418 ImGui::Text(tr("%s ROM Identity"), ICON_MD_VIDEOGAME_ASSET);
419 ImGui::Separator();
420
421 const char* roles[] = {"base", "dev", "patched", "release"};
422 int role_index = static_cast<int>(project_->rom_metadata.role);
423 if (ImGui::Combo(tr("Role"), &role_index, roles, IM_ARRAYSIZE(roles))) {
424 project_->rom_metadata.role = static_cast<project::RomRole>(role_index);
425 project_->Save();
426 }
427
428 const char* policies[] = {"allow", "warn", "block"};
429 int policy_index = static_cast<int>(project_->rom_metadata.write_policy);
430 if (ImGui::Combo(tr("Write Policy"), &policy_index, policies,
431 IM_ARRAYSIZE(policies))) {
433 static_cast<project::RomWritePolicy>(policy_index);
434 project_->Save();
435 }
436
437 std::string expected_hash = project_->rom_metadata.expected_hash;
438 if (ImGui::InputText(tr("Expected Hash"), &expected_hash)) {
439 project_->rom_metadata.expected_hash = expected_hash;
440 project_->Save();
441 }
442
443 static std::string cached_rom_hash;
444 static std::string cached_rom_path;
445 if (rom_ && rom_->is_loaded()) {
446 if (cached_rom_path != rom_->filename()) {
447 cached_rom_path = rom_->filename();
448 cached_rom_hash = util::ComputeRomHash(rom_->data(), rom_->size());
449 }
450 ImGui::Text(tr("Current ROM Hash: %s"), cached_rom_hash.empty()
451 ? "(unknown)"
452 : cached_rom_hash.c_str());
453 if (ImGui::Button(tr("Use Current ROM Hash"))) {
454 project_->rom_metadata.expected_hash = cached_rom_hash;
455 project_->Save();
456 }
457 } else {
458 ImGui::TextDisabled(tr("Current ROM Hash: (no ROM loaded)"));
459 }
460
461 ImGui::Spacing();
462 ImGui::Text(tr("%s Paths"), ICON_MD_FOLDER_OPEN);
463 ImGui::Separator();
464
465 // Output Folder
466 std::string output_folder = project_->output_folder;
467 if (ImGui::InputText(tr("Output Folder"), &output_folder)) {
468 project_->output_folder = output_folder;
469 project_->Save();
470 }
471
472 // Git Repository
473 std::string git_repo = project_->git_repository;
474 if (ImGui::InputText(tr("Git Repository"), &git_repo)) {
475 project_->git_repository = git_repo;
476 project_->Save();
477 }
478
479 ImGui::Spacing();
480 ImGui::Text(tr("%s Build"), ICON_MD_BUILD);
481 ImGui::Separator();
482
483 // Build Target
484 std::string build_target = project_->build_target;
485 if (ImGui::InputText(tr("Build Target (ROM)"), &build_target)) {
486 project_->build_target = build_target;
487 project_->Save();
488 }
489
490 // Symbols File
491 std::string symbols_file = project_->symbols_filename;
492 if (ImGui::InputText(tr("Symbols File"), &symbols_file)) {
493 project_->symbols_filename = symbols_file;
494 project_->Save();
495 }
496
497 ImGui::Spacing();
498 ImGui::Text(tr("%s ASM / Hack Manifest"), ICON_MD_CODE);
499 ImGui::Separator();
500 ImGui::TextWrapped(tr(
501 "Optional: load a hack manifest JSON (generated by an ASM project) to "
502 "annotate room tags, show feature flags, and surface which ROM regions "
503 "are owned by ASM vs safe to edit in yaze."));
504
505 std::string manifest_file = project_->hack_manifest_file;
506 if (ImGui::InputText(tr("Hack Manifest File"), &manifest_file)) {
507 project_->hack_manifest_file = manifest_file;
509 project_->Save();
510 }
511
512 const bool manifest_loaded = project_->hack_manifest.loaded();
513 ImGui::SameLine();
514 ImGui::TextDisabled(manifest_loaded ? "(loaded)" : "(not loaded)");
515 if (ImGui::Button(tr("Reload Manifest"))) {
517 }
518
519 if (manifest_loaded) {
520 ImGui::Spacing();
521 ImGui::Text(tr("Hack: %s"), project_->hack_manifest.hack_name().c_str());
522 ImGui::Text(tr("Manifest Version: %d"),
524 ImGui::Text(tr("Hooks Tracked: %d"), project_->hack_manifest.total_hooks());
525
526 const auto& pipeline = project_->hack_manifest.build_pipeline();
527 if (!pipeline.dev_rom.empty()) {
528 ImGui::Text(tr("Dev ROM: %s"), pipeline.dev_rom.c_str());
529 }
530 if (!pipeline.patched_rom.empty()) {
531 ImGui::Text(tr("Patched ROM: %s"), pipeline.patched_rom.c_str());
532 }
533 if (!pipeline.build_script.empty()) {
534 ImGui::Text(tr("Build Script: %s"), pipeline.build_script.c_str());
535 }
536
537 const auto& msg_layout = project_->hack_manifest.message_layout();
538 if (msg_layout.first_expanded_id != 0 || msg_layout.last_expanded_id != 0) {
539 ImGui::Text(tr("Expanded Messages: 0x%03X-0x%03X (%d)"),
540 msg_layout.first_expanded_id, msg_layout.last_expanded_id,
541 msg_layout.expanded_count);
542 }
543
544 if (ImGui::TreeNode(ICON_MD_FLAG " Hack Feature Flags")) {
545 for (const auto& flag : project_->hack_manifest.feature_flags()) {
546 ImGui::BulletText("%s = %d (%s)", flag.name.c_str(), flag.value,
547 flag.enabled ? "enabled" : "disabled");
548 if (!flag.source.empty()) {
549 ImGui::SameLine();
550 ImGui::TextDisabled("%s", flag.source.c_str());
551 }
552 }
553 ImGui::TreePop();
554 }
555
556 if (ImGui::TreeNode(ICON_MD_LABEL " Room Tags (Dispatch)")) {
557 for (const auto& tag : project_->hack_manifest.room_tags()) {
558 ImGui::BulletText(tr("0x%02X: %s"), tag.tag_id, tag.name.c_str());
559 if (!tag.enabled && !tag.feature_flag.empty()) {
560 ImGui::SameLine();
561 ImGui::TextDisabled(tr("(disabled by %s)"), tag.feature_flag.c_str());
562 }
563 if (!tag.purpose.empty() && ImGui::IsItemHovered()) {
564 ImGui::SetTooltip("%s", tag.purpose.c_str());
565 }
566 }
567 ImGui::TreePop();
568 }
569 }
570
571 ImGui::Spacing();
572 ImGui::Text(tr("%s Backup Settings"), ICON_MD_BACKUP);
573 ImGui::Separator();
574
575 std::string backup_folder = project_->rom_backup_folder;
576 if (ImGui::InputText(tr("Backup Folder"), &backup_folder)) {
577 project_->rom_backup_folder = backup_folder;
578 project_->Save();
579 }
580
581 bool backup_on_save = project_->workspace_settings.backup_on_save;
582 if (gui::DrawProperty("Backup Before Save", &backup_on_save)) {
584 project_->Save();
585 }
586
588 if (gui::DrawProperty("Retention Count", &retention,
589 {.min = 0, .max = 10000})) {
591 project_->Save();
592 }
593
595 if (gui::DrawProperty("Keep Daily Snapshots", &keep_daily)) {
597 project_->Save();
598 }
599
601 if (gui::DrawProperty("Keep Daily Days", &keep_days,
602 {.min = 1, .max = 3650})) {
604 project_->Save();
605 }
606
607 ImGui::Spacing();
608 ImGui::Text(tr("%s Dungeon Overlay"), ICON_MD_TRAIN);
609 ImGui::Separator();
610 ImGui::TextWrapped(
611 tr("Configure collision/object IDs used by minecart overlays and audits. "
612 "Hex values, ranges allowed (e.g. B0-BE)."));
613
614 static std::string overlay_project_path;
615 static HexListEditorState track_tiles_state;
616 static HexListEditorState stop_tiles_state;
617 static HexListEditorState switch_tiles_state;
618 static HexListEditorState track_object_state;
619 static HexListEditorState minecart_sprite_state;
620
621 if (overlay_project_path != project_->filepath) {
622 overlay_project_path = project_->filepath;
623 track_tiles_state.text =
624 FormatHexList(project_->dungeon_overlay.track_tiles);
625 stop_tiles_state.text =
627 switch_tiles_state.text =
629 track_object_state.text =
631 minecart_sprite_state.text =
633 track_tiles_state.error.clear();
634 stop_tiles_state.error.clear();
635 switch_tiles_state.error.clear();
636 track_object_state.error.clear();
637 minecart_sprite_state.error.clear();
638 }
639
640 auto draw_hex_list = [&](const char* label, const char* hint,
641 HexListEditorState& state,
642 const std::vector<uint16_t>& defaults,
643 std::vector<uint16_t>* target) {
644 if (!target) {
645 return;
646 }
647
648 bool apply = false;
649 ImGui::PushItemWidth(-180.0f);
650 if (ImGui::InputTextWithHint(label, hint, &state.text)) {
651 state.error.clear();
652 }
653 ImGui::PopItemWidth();
654 if (ImGui::IsItemDeactivatedAfterEdit()) {
655 apply = true;
656 }
657
658 ImGui::SameLine();
659 if (ImGui::SmallButton(absl::StrFormat("Apply##%s", label).c_str())) {
660 apply = true;
661 }
662 ImGui::SameLine();
663 if (ImGui::SmallButton(absl::StrFormat("Defaults##%s", label).c_str())) {
664 state.text = FormatHexList(defaults);
665 apply = true;
666 }
667 if (ImGui::IsItemHovered()) {
668 ImGui::SetTooltip(tr("Reset to defaults"));
669 }
670 ImGui::SameLine();
671 if (ImGui::SmallButton(absl::StrFormat("Clear##%s", label).c_str())) {
672 state.text.clear();
673 apply = true;
674 }
675 if (ImGui::IsItemHovered()) {
676 ImGui::SetTooltip(tr("Clear list (empty uses defaults)"));
677 }
678
679 const bool uses_defaults = target->empty();
680 const std::vector<uint16_t>& effective_values =
681 uses_defaults ? defaults : *target;
682 ImGui::SameLine();
683 ImGui::TextDisabled(ICON_MD_INFO);
684 if (ImGui::IsItemHovered()) {
685 ImGui::BeginTooltip();
686 ImGui::Text(tr("Effective: %s"), FormatHexList(effective_values).c_str());
687 if (uses_defaults) {
688 ImGui::TextDisabled(tr("Using defaults (list is empty)"));
689 }
690 ImGui::EndTooltip();
691 }
692
693 if (apply) {
694 std::vector<uint16_t> parsed;
695 std::string error;
696 if (ParseHexList(state.text, &parsed, &error)) {
697 *target = parsed;
698 project_->Save();
699 state.error.clear();
700 state.text = FormatHexList(parsed);
701 } else {
702 state.error = error;
703 }
704 }
705
706 if (!state.error.empty()) {
707 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.4f, 1.0f), "%s",
708 state.error.c_str());
709 }
710 };
711
712 draw_hex_list("Track Tiles", "0xB0-0xBE", track_tiles_state,
713 DefaultTrackTiles(), &project_->dungeon_overlay.track_tiles);
714 draw_hex_list("Stop Tiles", "0xB7, 0xB8, 0xB9, 0xBA", stop_tiles_state,
715 DefaultStopTiles(),
717 draw_hex_list("Switch Tiles", "0xD0-0xD3", switch_tiles_state,
718 DefaultSwitchTiles(),
720 draw_hex_list("Track Object IDs", "0x31", track_object_state,
721 DefaultTrackObjectIds(),
723 draw_hex_list("Minecart Sprite IDs", "0xA3", minecart_sprite_state,
724 DefaultMinecartSpriteIds(),
726}
727
729 if (!user_settings_) {
730 return;
731 }
732
733 auto& prefs = user_settings_->prefs();
734 auto& roots = prefs.project_root_paths;
735 static int selected_root_index = -1;
736 static std::string new_root_path;
737
738 ImGui::Text(tr("%s Project Roots"), ICON_MD_FOLDER_OPEN);
739 ImGui::Separator();
740
741 if (roots.empty()) {
742 ImGui::TextDisabled(tr("No project roots configured."));
743 }
744
745 if (ImGui::BeginChild("ProjectRootsList", ImVec2(0, 140), true)) {
746 for (size_t i = 0; i < roots.size(); ++i) {
747 const bool is_default = roots[i] == prefs.default_project_root;
748 std::string label =
750 if (is_default) {
751 label += " (default)";
752 }
753 if (ImGui::Selectable(label.c_str(),
754 selected_root_index == static_cast<int>(i))) {
755 selected_root_index = static_cast<int>(i);
756 }
757 }
758 }
759 ImGui::EndChild();
760
761 const bool has_selection =
762 selected_root_index >= 0 &&
763 selected_root_index < static_cast<int>(roots.size());
764 if (has_selection) {
765 if (ImGui::Button(tr("Set Default"))) {
766 prefs.default_project_root = roots[selected_root_index];
768 }
769 ImGui::SameLine();
770 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
771 const std::string removed = roots[selected_root_index];
772 roots.erase(roots.begin() + selected_root_index);
773 if (prefs.default_project_root == removed) {
774 prefs.default_project_root = roots.empty() ? "" : roots.front();
775 }
776 selected_root_index = roots.empty()
777 ? -1
778 : std::min(selected_root_index,
779 static_cast<int>(roots.size() - 1));
781 }
782 }
783
784 ImGui::Spacing();
785 ImGui::Text(tr("%s Add Root"), ICON_MD_ADD);
786 ImGui::Separator();
787
788 ImGui::InputTextWithHint("##project_root_add", "Add folder path...",
789 &new_root_path);
790 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
791 const std::string trimmed =
792 std::string(absl::StripAsciiWhitespace(new_root_path));
793 if (!trimmed.empty()) {
794 if (AddUniquePath(&roots, trimmed) &&
795 prefs.default_project_root.empty()) {
796 prefs.default_project_root = trimmed;
797 }
799 }
800 }
801 ImGui::SameLine();
802 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
803 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
804 if (!folder.empty()) {
805 if (AddUniquePath(&roots, folder) && prefs.default_project_root.empty()) {
806 prefs.default_project_root = folder;
807 }
809 }
810 }
811
812 ImGui::Spacing();
813 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
814 ImGui::Separator();
815
816 if (ImGui::Button(ICON_MD_HOME " Add Documents")) {
818 if (docs_dir.ok()) {
819 if (AddUniquePath(&roots, docs_dir->string()) &&
820 prefs.default_project_root.empty()) {
821 prefs.default_project_root = docs_dir->string();
822 }
824 }
825 }
826 ImGui::SameLine();
827 if (ImGui::Button(ICON_MD_CLOUD " Add iCloud Projects")) {
828 auto icloud_dir =
830 if (icloud_dir.ok()) {
831 if (AddUniquePath(&roots, icloud_dir->string())) {
832 prefs.default_project_root = icloud_dir->string();
833 }
835 }
836 }
837 ImGui::TextDisabled(
838 tr("iCloud projects live in Documents/Yaze/iCloud on this Mac."));
839
840 ImGui::Spacing();
841 ImGui::Text(tr("%s Sync Options"), ICON_MD_SYNC);
842 ImGui::Separator();
843
844 bool use_icloud_sync = prefs.use_icloud_sync;
845 if (ImGui::Checkbox(tr("Use iCloud sync (Documents)"), &use_icloud_sync)) {
846 prefs.use_icloud_sync = use_icloud_sync;
847 if (use_icloud_sync) {
848 auto icloud_dir =
850 if (icloud_dir.ok()) {
851 AddUniquePath(&roots, icloud_dir->string());
852 prefs.default_project_root = icloud_dir->string();
853 }
854 }
856 }
857
858 bool use_files_app = prefs.use_files_app;
859 if (ImGui::Checkbox(tr("Prefer Files app on iOS"), &use_files_app)) {
860 prefs.use_files_app = use_files_app;
862 }
863}
864
866 auto& theme_manager = gui::ThemeManager::Get();
867
868 ImGui::Text(tr("%s Theme Management"), ICON_MD_PALETTE);
869 ImGui::Separator();
870
871 // Current theme with color swatches
872 const auto& current = theme_manager.GetCurrentThemeName();
873 const auto& current_theme = theme_manager.GetCurrentTheme();
874
875 ImGui::Text(tr("Current Theme:"));
876 ImGui::SameLine();
877
878 // Draw 3 color swatches inline: primary, surface, accent
879 {
880 ImDrawList* draw_list = ImGui::GetWindowDrawList();
881 ImVec2 cursor = ImGui::GetCursorScreenPos();
882 const float swatch_size = 12.0f;
883 const float spacing = 2.0f;
884
885 auto draw_swatch = [&](const gui::Color& color, float offset_x) {
886 ImVec2 p_min(cursor.x + offset_x, cursor.y);
887 ImVec2 p_max(p_min.x + swatch_size, p_min.y + swatch_size);
888 ImU32 col =
889 ImGui::ColorConvertFloat4ToU32(gui::ConvertColorToImVec4(color));
890 draw_list->AddRectFilled(p_min, p_max, col);
891 draw_list->AddRect(
892 p_min, p_max,
893 ImGui::ColorConvertFloat4ToU32(ImVec4(0.5f, 0.5f, 0.5f, 0.6f)));
894 };
895
896 draw_swatch(current_theme.primary, 0.0f);
897 draw_swatch(current_theme.surface, swatch_size + spacing);
898 draw_swatch(current_theme.accent, 2.0f * (swatch_size + spacing));
899
900 // Advance cursor past the swatches
901 ImGui::Dummy(
902 ImVec2(3.0f * swatch_size + 2.0f * spacing + 4.0f, swatch_size));
903 }
904
905 ImGui::SameLine();
906 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", current.c_str());
907
908 ImGui::Spacing();
909
910 // Available themes list with hover preview and color swatches
911 ImGui::Text(tr("Available Themes:"));
912
913 bool any_theme_hovered = false;
914 if (ImGui::BeginChild("ThemeList", ImVec2(0, 200), true)) {
915 for (const auto& theme_name : theme_manager.GetAvailableThemes()) {
916 ImGui::PushID(theme_name.c_str());
917 bool is_current = (theme_name == current);
918
919 // Draw color swatches before the theme name
920 const gui::Theme* theme_data = theme_manager.GetTheme(theme_name);
921 if (theme_data) {
922 ImDrawList* draw_list = ImGui::GetWindowDrawList();
923 ImVec2 cursor = ImGui::GetCursorScreenPos();
924 const float swatch_size = 10.0f;
925 const float swatch_spacing = 2.0f;
926 const float total_swatch_width =
927 3.0f * swatch_size + 2.0f * swatch_spacing + 6.0f;
928
929 auto draw_small_swatch = [&](const gui::Color& color, float offset_x) {
930 ImVec2 p_min(cursor.x + offset_x, cursor.y + 2.0f);
931 ImVec2 p_max(p_min.x + swatch_size, p_min.y + swatch_size);
932 ImU32 col =
933 ImGui::ColorConvertFloat4ToU32(gui::ConvertColorToImVec4(color));
934 draw_list->AddRectFilled(p_min, p_max, col);
935 draw_list->AddRect(
936 p_min, p_max,
937 ImGui::ColorConvertFloat4ToU32(ImVec4(0.4f, 0.4f, 0.4f, 0.5f)));
938 };
939
940 draw_small_swatch(theme_data->primary, 0.0f);
941 draw_small_swatch(theme_data->surface, swatch_size + swatch_spacing);
942 draw_small_swatch(theme_data->accent,
943 2.0f * (swatch_size + swatch_spacing));
944
945 // Reserve space for swatches then draw the selectable
946 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + total_swatch_width);
947 }
948
949 // Checkmark prefix for the active theme
950 std::string label = is_current
951 ? std::string(ICON_MD_CHECK " ") + theme_name
952 : std::string(" ") + theme_name;
953
954 if (ImGui::Selectable(label.c_str(), is_current)) {
955 // If we're previewing, end preview first so the selected theme becomes
956 // the new baseline (otherwise EndPreview would restore the pre-preview
957 // theme when the cursor leaves the list).
958 if (theme_manager.IsPreviewActive()) {
959 theme_manager.EndPreview();
960 }
961 theme_manager.ApplyTheme(theme_name);
962 }
963
964 // Hover triggers live preview
965 if (ImGui::IsItemHovered()) {
966 any_theme_hovered = true;
967 theme_manager.StartPreview(theme_name);
968 }
969
970 ImGui::PopID();
971 }
972 }
973 ImGui::EndChild();
974
975 // Restore original theme when nothing is hovered
976 if (!any_theme_hovered && theme_manager.IsPreviewActive()) {
977 theme_manager.EndPreview();
978 }
979
980 // Refresh button
981 if (ImGui::Button(ICON_MD_REFRESH " Refresh Themes")) {
982 theme_manager.RefreshAvailableThemes();
983 }
984 if (ImGui::IsItemHovered()) {
985 ImGui::SetTooltip(
986 tr("Re-scan theme directories for new or changed themes"));
987 }
988
989 ImGui::Spacing();
990 ImGui::SeparatorText(tr("Display Density"));
991
992 {
993 auto preset = theme_manager.GetCurrentTheme().density_preset;
994 int density = static_cast<int>(preset);
995 bool changed = false;
996 changed |= ImGui::RadioButton(tr("Compact (0.75x)"), &density, 0);
997 ImGui::SameLine();
998 changed |= ImGui::RadioButton(tr("Normal (1.0x)"), &density, 1);
999 ImGui::SameLine();
1000 changed |= ImGui::RadioButton(tr("Comfortable (1.25x)"), &density, 2);
1001
1002 if (changed) {
1003 auto new_preset = static_cast<gui::DensityPreset>(density);
1004 auto theme = theme_manager.GetCurrentTheme();
1005 theme.ApplyDensityPreset(new_preset);
1006 theme_manager.ApplyTheme(theme);
1007 }
1008 }
1009
1010 ImGui::Spacing();
1011 ImGui::SeparatorText(tr("Editor/Workspace Motion"));
1012
1013 auto& prefs = user_settings_->prefs();
1014 bool reduced_motion = prefs.reduced_motion;
1015 if (ImGui::Checkbox(tr("Reduced Motion"), &reduced_motion)) {
1016 prefs.reduced_motion = reduced_motion;
1018 prefs.reduced_motion,
1019 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1021 }
1022 if (ImGui::IsItemHovered()) {
1023 ImGui::SetTooltip(
1024 tr("Disable panel/editor transition animations for a calmer editing "
1025 "experience."));
1026 }
1027
1028 int switch_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
1029 const char* switch_profile_labels[] = {"Snappy", "Standard", "Relaxed"};
1030 if (ImGui::Combo(tr("Switch Motion Profile"), &switch_profile,
1031 switch_profile_labels,
1032 IM_ARRAYSIZE(switch_profile_labels))) {
1033 prefs.switch_motion_profile = switch_profile;
1035 prefs.reduced_motion,
1036 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1038 }
1039 if (ImGui::IsItemHovered()) {
1040 ImGui::SetTooltip(
1041 tr("Controls editor/workspace switch timing and easing for panel fades "
1042 "and sidebar slides."));
1043 }
1044
1045 ImGui::Spacing();
1046 ImGui::Text(tr("%s Font"), ICON_MD_TEXT_FIELDS);
1047 ImGui::Separator();
1048
1049 if (user_settings_) {
1050 int font_index = user_settings_->prefs().font_family_index;
1051 if (gui::FontPicker("##font_family", &font_index)) {
1052 user_settings_->prefs().font_family_index = font_index;
1053 ::yaze::SetActiveFontIndex(font_index);
1055 }
1056
1057 ImGui::Text(tr("Global Font Scale"));
1058 float scale = user_settings_->prefs().font_global_scale;
1059 if (ImGui::SliderFloat("##global_font_scale", &scale, 0.5f, 2.0f, "%.2f")) {
1061 ImGui::GetIO().FontGlobalScale = scale;
1063 }
1064 }
1065
1066 ImGui::Spacing();
1067 ImGui::Text(tr("%s Status Bar"), ICON_MD_HORIZONTAL_RULE);
1068 ImGui::Separator();
1069
1070 bool show_status_bar = user_settings_->prefs().show_status_bar;
1071 if (ImGui::Checkbox(tr("Show Status Bar"), &show_status_bar)) {
1072 user_settings_->prefs().show_status_bar = show_status_bar;
1074 // Immediately apply to status bar if status_bar_ is available
1075 if (status_bar_) {
1076 status_bar_->SetEnabled(show_status_bar);
1077 }
1078 }
1079 if (ImGui::IsItemHovered()) {
1080 ImGui::SetTooltip(
1081 tr("Display ROM, session, cursor, and zoom info at bottom of window"));
1082 }
1083}
1084
1086 if (!user_settings_) {
1087 ImGui::TextDisabled(tr("UserSettings unavailable."));
1088 return;
1089 }
1090
1091 auto& prefs = user_settings_->prefs();
1092
1093 ImGui::Text(tr("%s Named Layouts"), ICON_MD_DASHBOARD);
1094 ImGui::Separator();
1095
1096 if (prefs.named_layouts.empty()) {
1097 ImGui::TextDisabled(
1098 tr("No saved layouts yet. Open the Layout Designer to capture one."));
1099 } else {
1100 // Sort the keys so the combo order is stable across frames
1101 // (named_layouts is unordered_map).
1102 std::vector<std::string> names;
1103 names.reserve(prefs.named_layouts.size());
1104 for (const auto& entry : prefs.named_layouts) {
1105 names.push_back(entry.first);
1106 }
1107 std::sort(names.begin(), names.end());
1108
1109 const std::string& active = prefs.last_applied_layout_name;
1110 const char* preview =
1111 active.empty() ? "(select a layout…)" : active.c_str();
1112
1113 if (ImGui::BeginCombo(tr("Active Layout"), preview)) {
1114 for (const auto& name : names) {
1115 const bool is_selected = (name == active);
1116 if (ImGui::Selectable(name.c_str(), is_selected)) {
1117 // Mirrors the LayoutManager startup-reapply path in shape:
1118 // lookup → parse → validate → ApplyDockTree. Inlined rather
1119 // than shared because the call sites have different error
1120 // surfacing — this one writes to a transient status string;
1121 // startup writes to the log and a one-shot consumed flag.
1123 }
1124 if (is_selected) {
1125 ImGui::SetItemDefaultFocus();
1126 }
1127 }
1128 ImGui::EndCombo();
1129 }
1130
1131 ImGui::SameLine();
1132 ImGui::BeginDisabled(active.empty() ||
1133 prefs.named_layouts.count(active) == 0);
1134 if (ImGui::SmallButton(tr("Re-apply"))) {
1136 }
1137 ImGui::EndDisabled();
1138 if (ImGui::IsItemHovered()) {
1139 ImGui::SetTooltip(
1140 tr("Re-apply the active layout (useful after closing or rearranging "
1141 "panels manually)."));
1142 }
1143 }
1144
1145 ImGui::Spacing();
1146 if (ImGui::Button(ICON_MD_DASHBOARD_CUSTOMIZE " Open Layout Designer")) {
1147 if (window_manager_ == nullptr) {
1148 workspace_status_message_ = "Designer unavailable: no window manager.";
1150 } else if (!window_manager_->IsWindowOpen("layout.designer") &&
1151 !window_manager_->OpenWindow("layout.designer")) {
1152 // OpenWindow returns false when the panel id isn't registered in
1153 // the active session. Surface that — silently doing nothing on
1154 // a missing registration would hide a real bug (panel renamed,
1155 // force-load archive misconfigured, etc.).
1157 "Open failed: \"layout.designer\" panel is not registered.";
1159 } else {
1160 window_manager_->SetWindowPinned("layout.designer", true);
1163 }
1164 }
1165 if (ImGui::IsItemHovered()) {
1166 ImGui::SetTooltip(
1167 tr("Open the Layout Designer panel to author or capture a workspace "
1168 "layout."));
1169 }
1170
1171 if (!workspace_status_message_.empty()) {
1172 ImGui::Spacing();
1174 ImGui::TextColored(gui::GetErrorColor(), "%s",
1176 } else {
1177 ImGui::TextDisabled("%s", workspace_status_message_.c_str());
1178 }
1179 }
1180}
1181
1182void SettingsPanel::ApplyNamedLayoutToDockspace(const std::string& name) {
1185 if (user_settings_ == nullptr) {
1186 workspace_status_message_ = "Apply failed: UserSettings unavailable.";
1188 return;
1189 }
1190 auto& prefs = user_settings_->prefs();
1191 const auto it = prefs.named_layouts.find(name);
1192 if (it == prefs.named_layouts.end()) {
1194 absl::StrCat("Apply failed: \"", name, "\" not found.");
1196 return;
1197 }
1198
1199 nlohmann::json parsed;
1200 try {
1201 parsed = nlohmann::json::parse(it->second);
1202 } catch (const nlohmann::json::parse_error& e) {
1204 absl::StrCat("Apply failed: invalid JSON (", e.what(), ").");
1206 return;
1207 }
1208
1209 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1210 if (!tree_or.ok()) {
1212 absl::StrCat("Apply failed: ", tree_or.status().message());
1214 return;
1215 }
1216
1217 std::string validation_error;
1218 if (!tree_or->Validate(&validation_error)) {
1220 absl::StrCat("Apply failed: invalid layout (", validation_error, ").");
1222 return;
1223 }
1224
1226 if (manager == nullptr) {
1227 workspace_status_message_ = "Apply failed: LayoutManager unavailable.";
1229 return;
1230 }
1231 const ImGuiID dockspace_id = manager->GetMainDockspaceId();
1232 if (dockspace_id == 0) {
1233 workspace_status_message_ = "Apply failed: main dockspace not yet created.";
1235 return;
1236 }
1237
1238 const absl::Status apply_status =
1239 manager->ApplyDockTree(*tree_or, dockspace_id);
1240 if (!apply_status.ok()) {
1242 absl::StrCat("Apply failed: ", apply_status.message());
1244 return;
1245 }
1246
1247 prefs.last_applied_layout_name = name;
1248 (void)user_settings_->Save();
1249 workspace_status_message_ = absl::StrCat("Applied \"", name, "\".");
1251}
1252
1254 if (!user_settings_)
1255 return;
1256
1257 ImGui::Text(tr("%s Auto-Save"), ICON_MD_SAVE);
1258 ImGui::Separator();
1259
1260 if (ImGui::Checkbox(tr("Enable Auto-Save"),
1263 }
1264
1266 ImGui::Indent();
1267 int interval = static_cast<int>(user_settings_->prefs().autosave_interval);
1268 if (ImGui::SliderInt(tr("Interval (sec)"), &interval, 60, 600)) {
1269 user_settings_->prefs().autosave_interval = static_cast<float>(interval);
1271 }
1272
1273 if (ImGui::Checkbox(tr("Backup Before Save"),
1276 }
1277 ImGui::Unindent();
1278 }
1279
1280 ImGui::Spacing();
1281 ImGui::Text(tr("%s Recent Files"), ICON_MD_HISTORY);
1282 ImGui::Separator();
1283
1284 if (ImGui::SliderInt(tr("Limit"), &user_settings_->prefs().recent_files_limit,
1285 5, 50)) {
1287 }
1288
1289 ImGui::Spacing();
1290 ImGui::Text(tr("%s Default Editor"), ICON_MD_EDIT);
1291 ImGui::Separator();
1292
1293 const char* editors[] = {"None", "Overworld", "Dungeon", "Graphics"};
1294 if (ImGui::Combo("##DefaultEditor", &user_settings_->prefs().default_editor,
1295 editors, IM_ARRAYSIZE(editors))) {
1297 }
1298
1299 ImGui::Spacing();
1300 ImGui::Text(tr("%s Sprite Names"), ICON_MD_LABEL);
1301 ImGui::Separator();
1302 if (ImGui::Checkbox(tr("Use HMagic sprite names (expanded)"),
1307 }
1308}
1309
1311 if (!user_settings_)
1312 return;
1313
1314 ImGui::Text(tr("%s Graphics"), ICON_MD_IMAGE);
1315 ImGui::Separator();
1316
1317 if (ImGui::Checkbox(tr("V-Sync"), &user_settings_->prefs().vsync)) {
1319 }
1320
1321 if (ImGui::SliderInt(tr("Target FPS"), &user_settings_->prefs().target_fps,
1322 30, 144)) {
1324 }
1325
1326 ImGui::Spacing();
1327 ImGui::Text(tr("%s Memory"), ICON_MD_MEMORY);
1328 ImGui::Separator();
1329
1330 if (ImGui::SliderInt(tr("Cache Size (MB)"),
1331 &user_settings_->prefs().cache_size_mb, 128, 2048)) {
1333 }
1334
1335 if (ImGui::SliderInt(tr("Undo History"),
1336 &user_settings_->prefs().undo_history_size, 10, 200)) {
1338 }
1339
1340 ImGui::Spacing();
1341 ImGui::Separator();
1342 ImGui::Text(tr("Current FPS: %.1f"), ImGui::GetIO().Framerate);
1343 ImGui::Text(tr("Frame Time: %.3f ms"), 1000.0f / ImGui::GetIO().Framerate);
1344}
1345
1347 if (!user_settings_)
1348 return;
1349
1350 auto& prefs = user_settings_->prefs();
1351 auto& hosts = prefs.ai_hosts;
1352 static int selected_host_index = -1;
1353
1354 auto draw_key_row = [&](const char* label, std::string* key,
1355 const char* env_var, const char* id) {
1356 ImGui::PushID(id);
1357 ImGui::Text("%s", label);
1358 const ImVec2 button_size = ImGui::CalcTextSize(ICON_MD_SYNC " Env");
1359 float env_button_width =
1360 button_size.x + ImGui::GetStyle().FramePadding.x * 2.0f;
1361 float input_width = ImGui::GetContentRegionAvail().x - env_button_width -
1362 ImGui::GetStyle().ItemSpacing.x;
1363 bool stack = input_width < 160.0f;
1364 ImGui::SetNextItemWidth(stack ? -1.0f : input_width);
1365 if (ImGui::InputTextWithHint("##key", "API key...", key,
1366 ImGuiInputTextFlags_Password)) {
1368 }
1369 if (!stack) {
1370 ImGui::SameLine();
1371 }
1372 if (ImGui::SmallButton(ICON_MD_SYNC " Env")) {
1373 const char* env_key = std::getenv(env_var);
1374 if (env_key) {
1375 *key = env_key;
1377 }
1378 }
1379 ImGui::Spacing();
1380 ImGui::PopID();
1381 };
1382
1383 ImGui::Text(tr("%s Provider Keys"), ICON_MD_VPN_KEY);
1384 ImGui::Separator();
1385 draw_key_row("OpenAI", &prefs.openai_api_key, "OPENAI_API_KEY", "openai_key");
1386 draw_key_row("Anthropic", &prefs.anthropic_api_key, "ANTHROPIC_API_KEY",
1387 "anthropic_key");
1388 draw_key_row("Google (Gemini)", &prefs.gemini_api_key, "GEMINI_API_KEY",
1389 "gemini_key");
1390 ImGui::Spacing();
1391
1392 // Provider selection
1393 ImGui::Text(tr("%s Provider Defaults (legacy)"), ICON_MD_CLOUD);
1394 ImGui::Separator();
1395
1396 const char* providers[] = {"Ollama (Local)", "Gemini (Cloud)",
1397 "Mock (Testing)"};
1398 if (ImGui::Combo("##Provider", &prefs.ai_provider, providers,
1399 IM_ARRAYSIZE(providers))) {
1401 }
1402
1403 ImGui::Spacing();
1404 ImGui::Text(tr("%s Host Routing"), ICON_MD_STORAGE);
1405 ImGui::Separator();
1406
1407 const char* active_preview = "None";
1408 const char* remote_preview = "None";
1409 for (const auto& host : hosts) {
1410 if (!prefs.active_ai_host_id.empty() &&
1411 host.id == prefs.active_ai_host_id) {
1412 active_preview = host.label.c_str();
1413 }
1414 if (!prefs.remote_build_host_id.empty() &&
1415 host.id == prefs.remote_build_host_id) {
1416 remote_preview = host.label.c_str();
1417 }
1418 }
1419
1420 if (ImGui::BeginCombo(tr("Active Host"), active_preview)) {
1421 for (size_t i = 0; i < hosts.size(); ++i) {
1422 const bool is_selected = (!prefs.active_ai_host_id.empty() &&
1423 hosts[i].id == prefs.active_ai_host_id);
1424 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1425 prefs.active_ai_host_id = hosts[i].id;
1426 if (prefs.remote_build_host_id.empty()) {
1427 prefs.remote_build_host_id = hosts[i].id;
1428 }
1430 }
1431 if (is_selected) {
1432 ImGui::SetItemDefaultFocus();
1433 }
1434 }
1435 ImGui::EndCombo();
1436 }
1437
1438 if (ImGui::BeginCombo(tr("Remote Build Host"), remote_preview)) {
1439 for (size_t i = 0; i < hosts.size(); ++i) {
1440 const bool is_selected = (!prefs.remote_build_host_id.empty() &&
1441 hosts[i].id == prefs.remote_build_host_id);
1442 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1443 prefs.remote_build_host_id = hosts[i].id;
1445 }
1446 if (is_selected) {
1447 ImGui::SetItemDefaultFocus();
1448 }
1449 }
1450 ImGui::EndCombo();
1451 }
1452
1453 ImGui::Spacing();
1454 ImGui::Text(tr("%s AI Hosts"), ICON_MD_STORAGE);
1455 ImGui::Separator();
1456
1457 if (selected_host_index >= static_cast<int>(hosts.size())) {
1458 selected_host_index = hosts.empty() ? -1 : 0;
1459 }
1460 if (selected_host_index < 0 && !hosts.empty()) {
1461 for (size_t i = 0; i < hosts.size(); ++i) {
1462 if (!prefs.active_ai_host_id.empty() &&
1463 hosts[i].id == prefs.active_ai_host_id) {
1464 selected_host_index = static_cast<int>(i);
1465 break;
1466 }
1467 }
1468 if (selected_host_index < 0) {
1469 selected_host_index = 0;
1470 }
1471 }
1472
1473 ImGui::BeginChild("##ai_host_list", ImVec2(0, 150), true);
1474 for (size_t i = 0; i < hosts.size(); ++i) {
1475 const bool is_selected = static_cast<int>(i) == selected_host_index;
1476 std::string label = hosts[i].label;
1477 if (hosts[i].id == prefs.active_ai_host_id) {
1478 label += " (active)";
1479 }
1480 if (hosts[i].id == prefs.remote_build_host_id) {
1481 label += " (build)";
1482 }
1483 if (ImGui::Selectable(label.c_str(), is_selected)) {
1484 selected_host_index = static_cast<int>(i);
1485 }
1486 std::string tags = BuildHostTagString(hosts[i]);
1487 if (!tags.empty()) {
1488 ImGui::SameLine();
1489 ImGui::TextDisabled("%s", tags.c_str());
1490 }
1491 }
1492 ImGui::EndChild();
1493
1494 auto add_host = [&](UserSettings::Preferences::AiHost host) {
1495 if (host.id.empty()) {
1496 host.id = absl::StrFormat("host-%zu", hosts.size() + 1);
1497 }
1498 hosts.push_back(host);
1499 selected_host_index = static_cast<int>(hosts.size() - 1);
1500 if (prefs.active_ai_host_id.empty()) {
1501 prefs.active_ai_host_id = host.id;
1502 }
1503 if (prefs.remote_build_host_id.empty()) {
1504 prefs.remote_build_host_id = host.id;
1505 }
1507 };
1508
1509 if (ImGui::Button(ICON_MD_ADD " Add Host")) {
1511 host.label = "New Host";
1512 host.base_url = "http://localhost:1234";
1513 host.api_type = "openai";
1514 add_host(host);
1515 }
1516 ImGui::SameLine();
1517 if (ImGui::Button(ICON_MD_DELETE " Remove") && selected_host_index >= 0 &&
1518 selected_host_index < static_cast<int>(hosts.size())) {
1519 const std::string removed_id = hosts[selected_host_index].id;
1520 hosts.erase(hosts.begin() + selected_host_index);
1521 if (prefs.active_ai_host_id == removed_id) {
1522 prefs.active_ai_host_id = hosts.empty() ? "" : hosts.front().id;
1523 }
1524 if (prefs.remote_build_host_id == removed_id) {
1525 prefs.remote_build_host_id = prefs.active_ai_host_id;
1526 }
1527 selected_host_index =
1528 hosts.empty()
1529 ? -1
1530 : std::min(selected_host_index, static_cast<int>(hosts.size() - 1));
1532 }
1533
1534 ImGui::SameLine();
1535 if (ImGui::Button(tr("Add LM Studio"))) {
1537 host.label = "LM Studio (local)";
1538 host.base_url = "http://localhost:1234";
1540 host.supports_tools = true;
1541 host.supports_streaming = true;
1542 add_host(host);
1543 }
1544 ImGui::SameLine();
1545 if (ImGui::Button(tr("Add AFS Bridge"))) {
1547 host.label = "halext AFS Bridge";
1548 host.base_url = "https://halext.org";
1550 host.supports_tools = true;
1551 host.supports_streaming = true;
1552 add_host(host);
1553 }
1554 ImGui::SameLine();
1555 if (ImGui::Button(tr("Add Ollama"))) {
1557 host.label = "Ollama (local)";
1558 host.base_url = "http://localhost:11434";
1560 host.supports_tools = true;
1561 host.supports_streaming = true;
1562 add_host(host);
1563 }
1564
1565 static std::string tailscale_host;
1566 ImGui::InputTextWithHint("##tailscale_host", "host.ts.net:1234",
1567 &tailscale_host);
1568 ImGui::SameLine();
1569 if (ImGui::Button(tr("Add Tailscale Host"))) {
1570 std::string trimmed =
1571 std::string(absl::StripAsciiWhitespace(tailscale_host));
1572 if (!trimmed.empty()) {
1574 host.label = "Tailscale Host";
1575 if (absl::StrContains(trimmed, "://")) {
1576 host.base_url = trimmed;
1577 } else {
1578 host.base_url = "http://" + trimmed;
1579 }
1581 host.supports_tools = true;
1582 host.supports_streaming = true;
1583 host.allow_insecure = true;
1584 add_host(host);
1585 tailscale_host.clear();
1586 }
1587 }
1588
1589 if (selected_host_index >= 0 &&
1590 selected_host_index < static_cast<int>(hosts.size())) {
1591 auto& host = hosts[static_cast<size_t>(selected_host_index)];
1592 ImGui::Spacing();
1593 ImGui::Text(tr("Host Details"));
1594 ImGui::Separator();
1595 if (ImGui::InputText(tr("Label"), &host.label)) {
1597 }
1598 if (ImGui::InputText(tr("Base URL"), &host.base_url)) {
1600 }
1601
1602 const char* api_types[] = {"openai", "ollama", "gemini",
1603 "anthropic", "lmstudio", "grpc"};
1604 int api_index = 0;
1605 for (int i = 0; i < IM_ARRAYSIZE(api_types); ++i) {
1606 if (host.api_type == api_types[i]) {
1607 api_index = i;
1608 break;
1609 }
1610 }
1611 if (ImGui::Combo(tr("API Type"), &api_index, api_types,
1612 IM_ARRAYSIZE(api_types))) {
1613 host.api_type = api_types[api_index];
1615 }
1616
1617 if (ImGui::InputText(tr("API Key"), &host.api_key,
1618 ImGuiInputTextFlags_Password)) {
1620 }
1621 if (ImGui::InputText(tr("Keychain ID"), &host.credential_id)) {
1623 }
1624 ImGui::SameLine();
1625 if (ImGui::SmallButton(tr("Use Host ID"))) {
1626 host.credential_id = host.id;
1628 }
1629 if (!host.credential_id.empty() && host.api_key.empty()) {
1630 ImGui::TextDisabled(tr("Keychain lookup enabled (leave API key empty)."));
1631 }
1632
1633 if (ImGui::Checkbox(tr("Supports Vision"), &host.supports_vision)) {
1635 }
1636 ImGui::SameLine();
1637 if (ImGui::Checkbox(tr("Supports Tools"), &host.supports_tools)) {
1639 }
1640 ImGui::SameLine();
1641 if (ImGui::Checkbox(tr("Supports Streaming"), &host.supports_streaming)) {
1643 }
1644 if (ImGui::Checkbox(tr("Allow Insecure HTTP"), &host.allow_insecure)) {
1646 }
1647 }
1648
1649 ImGui::Spacing();
1650 ImGui::Text(tr("%s Local Model Paths"), ICON_MD_FOLDER);
1651 ImGui::Separator();
1652
1653 auto& model_paths = prefs.ai_model_paths;
1654 static int selected_model_path = -1;
1655 static std::string new_model_path;
1656
1657 if (model_paths.empty()) {
1658 ImGui::TextDisabled(tr("No model paths configured."));
1659 }
1660
1661 if (ImGui::BeginChild("ModelPathsList", ImVec2(0, 120), true)) {
1662 for (size_t i = 0; i < model_paths.size(); ++i) {
1663 std::string label =
1665 if (ImGui::Selectable(label.c_str(),
1666 selected_model_path == static_cast<int>(i))) {
1667 selected_model_path = static_cast<int>(i);
1668 }
1669 }
1670 }
1671 ImGui::EndChild();
1672
1673 const bool has_model_selection =
1674 selected_model_path >= 0 &&
1675 selected_model_path < static_cast<int>(model_paths.size());
1676 if (has_model_selection) {
1677 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
1678 model_paths.erase(model_paths.begin() + selected_model_path);
1679 selected_model_path =
1680 model_paths.empty()
1681 ? -1
1682 : std::min(selected_model_path,
1683 static_cast<int>(model_paths.size() - 1));
1685 }
1686 }
1687
1688 ImGui::Spacing();
1689 ImGui::InputTextWithHint("##model_path_add", "Add folder path...",
1690 &new_model_path);
1691 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
1692 const std::string trimmed =
1693 std::string(absl::StripAsciiWhitespace(new_model_path));
1694 if (!trimmed.empty() && AddUniquePath(&model_paths, trimmed)) {
1696 new_model_path.clear();
1697 }
1698 }
1699 ImGui::SameLine();
1700 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
1701 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
1702 if (!folder.empty() && AddUniquePath(&model_paths, folder)) {
1704 }
1705 }
1706
1707 ImGui::Spacing();
1708 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
1709 ImGui::Separator();
1710 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
1711 if (ImGui::Button(ICON_MD_HOME " Add ~/models")) {
1712 if (!home_dir.empty() && home_dir != ".") {
1713 if (AddUniquePath(&model_paths, (home_dir / "models").string())) {
1715 }
1716 }
1717 }
1718 ImGui::SameLine();
1719 if (ImGui::Button(tr("Add ~/.lmstudio/models"))) {
1720 if (!home_dir.empty() && home_dir != ".") {
1721 if (AddUniquePath(&model_paths,
1722 (home_dir / ".lmstudio" / "models").string())) {
1724 }
1725 }
1726 }
1727 ImGui::SameLine();
1728 if (ImGui::Button(tr("Add ~/.ollama/models"))) {
1729 if (!home_dir.empty() && home_dir != ".") {
1730 if (AddUniquePath(&model_paths,
1731 (home_dir / ".ollama" / "models").string())) {
1733 }
1734 }
1735 }
1736
1737 ImGui::Spacing();
1738 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
1739 ImGui::Separator();
1740
1741 if (ImGui::SliderFloat(tr("Temperature"),
1742 &user_settings_->prefs().ai_temperature, 0.0f, 2.0f)) {
1744 }
1745 ImGui::TextDisabled(tr("Higher = more creative"));
1746
1747 if (ImGui::SliderInt(tr("Max Tokens"), &user_settings_->prefs().ai_max_tokens,
1748 256, 8192)) {
1750 }
1751
1752 ImGui::Spacing();
1753 ImGui::Text(tr("%s Behavior"), ICON_MD_PSYCHOLOGY);
1754 ImGui::Separator();
1755
1756 if (ImGui::Checkbox(tr("Proactive Suggestions"),
1759 }
1760
1761 if (ImGui::Checkbox(tr("Auto-Learn Preferences"),
1764 }
1765
1766 if (ImGui::Checkbox(tr("Enable Vision"),
1769 }
1770
1771 ImGui::Spacing();
1772 ImGui::Text(tr("%s Logging"), ICON_MD_TERMINAL);
1773 ImGui::Separator();
1774
1775 const char* log_levels[] = {"Debug", "Info", "Warning", "Error", "Fatal"};
1776 if (ImGui::Combo(tr("Log Level"), &user_settings_->prefs().log_level,
1777 log_levels, IM_ARRAYSIZE(log_levels))) {
1778 // Apply log level logic here if needed
1780 }
1781}
1782
1784 if (ImGui::TreeNodeEx(ICON_MD_KEYBOARD " Shortcuts",
1785 ImGuiTreeNodeFlags_DefaultOpen)) {
1786 ImGui::InputTextWithHint("##shortcut_filter", "Filter shortcuts...",
1788 if (ImGui::IsItemHovered()) {
1789 ImGui::SetTooltip(tr("Filter by action name or key combo"));
1790 }
1791 ImGui::Spacing();
1792
1793 if (ImGui::TreeNode("Global Shortcuts")) {
1795 ImGui::TreePop();
1796 }
1797 if (ImGui::TreeNode("Editor Shortcuts")) {
1799 ImGui::TreePop();
1800 }
1801 if (ImGui::TreeNode("Panel Shortcuts")) {
1803 ImGui::TreePop();
1804 }
1805 ImGui::TextDisabled(
1806 tr("Tip: Use Cmd/Opt labels on macOS or Ctrl/Alt on Windows/Linux. "
1807 "Function keys and symbols (/, -) are supported."));
1808 ImGui::TreePop();
1809 }
1810}
1811
1812bool SettingsPanel::MatchesShortcutFilter(const std::string& text) const {
1813 if (shortcut_filter_.empty()) {
1814 return true;
1815 }
1816 std::string haystack = absl::AsciiStrToLower(text);
1817 std::string needle = absl::AsciiStrToLower(shortcut_filter_);
1818 return absl::StrContains(haystack, needle);
1819}
1820
1823 ImGui::TextDisabled(tr("Not available"));
1824 return;
1825 }
1826
1827 auto shortcuts =
1829 if (shortcuts.empty()) {
1830 ImGui::TextDisabled(tr("No global shortcuts registered."));
1831 return;
1832 }
1833
1834 static std::unordered_map<std::string, std::string> editing;
1835
1836 bool has_match = false;
1837 for (const auto& sc : shortcuts) {
1838 std::string label = sc.name;
1839 std::string keys = PrintShortcut(sc.keys);
1840 if (!MatchesShortcutFilter(label) && !MatchesShortcutFilter(keys)) {
1841 continue;
1842 }
1843 has_match = true;
1844 auto it = editing.find(sc.name);
1845 if (it == editing.end()) {
1846 std::string current = PrintShortcut(sc.keys);
1847 // Use user override if present
1848 auto u = user_settings_->prefs().global_shortcuts.find(sc.name);
1849 if (u != user_settings_->prefs().global_shortcuts.end()) {
1850 current = u->second;
1851 }
1852 editing[sc.name] = current;
1853 }
1854
1855 ImGui::PushID(sc.name.c_str());
1856 ImGui::Text("%s", sc.name.c_str());
1857 ImGui::SameLine();
1858 ImGui::SetNextItemWidth(180);
1859 std::string& value = editing[sc.name];
1860 if (ImGui::InputText("##global", &value,
1861 ImGuiInputTextFlags_EnterReturnsTrue |
1862 ImGuiInputTextFlags_AutoSelectAll)) {
1863 auto parsed = ParseShortcut(value);
1864 if (!parsed.empty() || value.empty()) {
1865 // Empty string clears the shortcut
1866 shortcut_manager_->UpdateShortcutKeys(sc.name, parsed);
1867 if (value.empty()) {
1868 user_settings_->prefs().global_shortcuts.erase(sc.name);
1869 } else {
1870 user_settings_->prefs().global_shortcuts[sc.name] = value;
1871 }
1873 }
1874 }
1875 ImGui::PopID();
1876 }
1877 if (!has_match) {
1878 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
1879 }
1880}
1881
1884 ImGui::TextDisabled(tr("Not available"));
1885 return;
1886 }
1887
1888 auto shortcuts =
1890 std::map<std::string, std::vector<Shortcut>> grouped;
1891 static std::unordered_map<std::string, std::string> editing;
1892
1893 for (const auto& sc : shortcuts) {
1894 auto pos = sc.name.find(".");
1895 std::string group =
1896 pos != std::string::npos ? sc.name.substr(0, pos) : "general";
1897 grouped[group].push_back(sc);
1898 }
1899 bool has_match = false;
1900 for (const auto& [group, list] : grouped) {
1901 std::vector<Shortcut> filtered;
1902 filtered.reserve(list.size());
1903 for (const auto& sc : list) {
1904 std::string keys = PrintShortcut(sc.keys);
1905 if (MatchesShortcutFilter(sc.name) || MatchesShortcutFilter(keys)) {
1906 filtered.push_back(sc);
1907 }
1908 }
1909 if (filtered.empty()) {
1910 continue;
1911 }
1912 has_match = true;
1913 if (ImGui::TreeNode(group.c_str())) {
1914 for (const auto& sc : filtered) {
1915 ImGui::PushID(sc.name.c_str());
1916 ImGui::Text("%s", sc.name.c_str());
1917 ImGui::SameLine();
1918 ImGui::SetNextItemWidth(180);
1919 std::string& value = editing[sc.name];
1920 if (value.empty()) {
1921 value = PrintShortcut(sc.keys);
1922 // Apply user override if present
1923 auto u = user_settings_->prefs().editor_shortcuts.find(sc.name);
1924 if (u != user_settings_->prefs().editor_shortcuts.end()) {
1925 value = u->second;
1926 }
1927 }
1928 if (ImGui::InputText("##editor", &value,
1929 ImGuiInputTextFlags_EnterReturnsTrue |
1930 ImGuiInputTextFlags_AutoSelectAll)) {
1931 auto parsed = ParseShortcut(value);
1932 if (!parsed.empty() || value.empty()) {
1933 shortcut_manager_->UpdateShortcutKeys(sc.name, parsed);
1934 if (value.empty()) {
1935 user_settings_->prefs().editor_shortcuts.erase(sc.name);
1936 } else {
1937 user_settings_->prefs().editor_shortcuts[sc.name] = value;
1938 }
1940 }
1941 }
1942 ImGui::PopID();
1943 }
1944 ImGui::TreePop();
1945 }
1946 }
1947 if (!has_match) {
1948 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
1949 }
1950}
1951
1954 ImGui::TextDisabled(tr("Registry not available"));
1955 return;
1956 }
1957
1958 // Simplified shortcut editor for sidebar
1959 auto categories = window_manager_->GetAllCategories();
1960
1961 bool has_match = false;
1962 for (const auto& category : categories) {
1963 auto cards = window_manager_->GetWindowsInCategory(0, category);
1964 std::vector<decltype(cards)::value_type> filtered_cards;
1965 filtered_cards.reserve(cards.size());
1966 for (const auto& card : cards) {
1967 if (MatchesShortcutFilter(card.display_name) ||
1968 MatchesShortcutFilter(card.card_id)) {
1969 filtered_cards.push_back(card);
1970 }
1971 }
1972 if (filtered_cards.empty()) {
1973 continue;
1974 }
1975 has_match = true;
1976 if (ImGui::TreeNode(category.c_str())) {
1977
1978 for (const auto& card : filtered_cards) {
1979 ImGui::PushID(card.card_id.c_str());
1980
1981 ImGui::Text("%s %s", card.icon.c_str(), card.display_name.c_str());
1982
1983 std::string current_shortcut;
1984 auto it = user_settings_->prefs().panel_shortcuts.find(card.card_id);
1985 if (it != user_settings_->prefs().panel_shortcuts.end()) {
1986 current_shortcut = it->second;
1987 } else if (!card.shortcut_hint.empty()) {
1988 current_shortcut = card.shortcut_hint;
1989 } else {
1990 current_shortcut = "None";
1991 }
1992
1993 // Display platform-aware label
1994 std::string display_shortcut = current_shortcut;
1995 auto parsed = ParseShortcut(current_shortcut);
1996 if (!parsed.empty()) {
1997 display_shortcut = PrintShortcut(parsed);
1998 }
1999
2000 if (is_editing_shortcut_ && editing_card_id_ == card.card_id) {
2001 ImGui::SetNextItemWidth(120);
2002 ImGui::SetKeyboardFocusHere();
2003 if (ImGui::InputText("##Edit", shortcut_edit_buffer_,
2004 sizeof(shortcut_edit_buffer_),
2005 ImGuiInputTextFlags_EnterReturnsTrue)) {
2006 if (strlen(shortcut_edit_buffer_) > 0) {
2007 user_settings_->prefs().panel_shortcuts[card.card_id] =
2009 } else {
2010 user_settings_->prefs().panel_shortcuts.erase(card.card_id);
2011 }
2013 is_editing_shortcut_ = false;
2014 editing_card_id_.clear();
2015 }
2016 ImGui::SameLine();
2017 if (ImGui::Button(ICON_MD_CLOSE)) {
2018 is_editing_shortcut_ = false;
2019 editing_card_id_.clear();
2020 }
2021 } else {
2022 if (ImGui::Button(display_shortcut.c_str(), ImVec2(120, 0))) {
2023 is_editing_shortcut_ = true;
2024 editing_card_id_ = card.card_id;
2025 strncpy(shortcut_edit_buffer_, current_shortcut.c_str(),
2026 sizeof(shortcut_edit_buffer_) - 1);
2027 }
2028 if (ImGui::IsItemHovered()) {
2029 ImGui::SetTooltip(tr("Click to edit shortcut"));
2030 }
2031 }
2032
2033 ImGui::PopID();
2034 }
2035
2036 ImGui::TreePop();
2037 }
2038 }
2039 if (!has_match) {
2040 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
2041 }
2042}
2043
2045 // Load patches on first access
2046 if (!patches_loaded_) {
2047 // Try to load from default patches location
2048 auto patches_dir_status = util::PlatformPaths::FindAsset("patches");
2049 if (patches_dir_status.ok()) {
2050 auto status = patch_manager_.LoadPatches(patches_dir_status->string());
2051 if (status.ok()) {
2052 patches_loaded_ = true;
2053 if (!patch_manager_.folders().empty()) {
2055 }
2056 }
2057 }
2058 }
2059
2060 ImGui::Text(tr("%s ZScream Patch System"), ICON_MD_EXTENSION);
2061 ImGui::Separator();
2062
2063 if (!patches_loaded_) {
2064 ImGui::TextDisabled(tr("No patches loaded"));
2065 ImGui::TextDisabled(tr("Place .asm patches in assets/patches/"));
2066
2067 if (ImGui::Button(tr("Browse for Patches Folder..."))) {
2068 // TODO: File browser for patches folder
2069 }
2070 return;
2071 }
2072
2073 // Status line
2074 int enabled_count = patch_manager_.GetEnabledPatchCount();
2075 int total_count = static_cast<int>(patch_manager_.patches().size());
2076 ImGui::Text(tr("Loaded: %d patches (%d enabled)"), total_count,
2077 enabled_count);
2078
2079 ImGui::Spacing();
2080
2081 // Folder tabs
2082 if (gui::BeginThemedTabBar("##PatchFolders",
2083 ImGuiTabBarFlags_FittingPolicyScroll)) {
2084 for (const auto& folder : patch_manager_.folders()) {
2085 if (ImGui::BeginTabItem(folder.c_str())) {
2086 selected_folder_ = folder;
2087 DrawPatchList(folder);
2088 ImGui::EndTabItem();
2089 }
2090 }
2092 }
2093
2094 ImGui::Spacing();
2095 ImGui::Separator();
2096
2097 // Selected patch details
2098 if (selected_patch_) {
2100 } else {
2101 ImGui::TextDisabled(tr("Select a patch to view details"));
2102 }
2103
2104 ImGui::Spacing();
2105 ImGui::Separator();
2106
2107 // Action buttons
2108 if (ImGui::Button(ICON_MD_CHECK " Apply Patches to ROM")) {
2109 if (rom_ && rom_->is_loaded()) {
2110#ifdef YAZE_WITH_Z3DK
2111 if (project_) {
2113 const auto& z3dk = project_->z3dk_settings;
2114 options.include_paths = z3dk.include_paths;
2115 options.defines = z3dk.defines;
2116 options.std_includes_path = z3dk.std_includes_path;
2117 options.std_defines_path = z3dk.std_defines_path;
2118 options.mapper = z3dk.mapper;
2119 options.rom_size = z3dk.rom_size;
2120 options.capture_nocash_symbols = (z3dk.symbols_format == "nocash");
2121 options.warn_unused_symbols = z3dk.warn_unused_symbols;
2122 options.warn_branch_outside_bank = z3dk.warn_branch_outside_bank;
2123 options.warn_unknown_width = z3dk.warn_unknown_width;
2124 options.warn_org_collision = z3dk.warn_org_collision;
2125 options.warn_unauthorized_hook = z3dk.warn_unauthorized_hook;
2126 options.warn_stack_balance = z3dk.warn_stack_balance;
2127 options.warn_hook_return = z3dk.warn_hook_return;
2128 for (const auto& range : z3dk.prohibited_memory_ranges) {
2129 options.prohibited_memory_ranges.push_back(
2130 {.start = range.start, .end = range.end, .reason = range.reason});
2131 }
2132 if (!project_->code_folder.empty() &&
2133 std::find(options.include_paths.begin(),
2134 options.include_paths.end(),
2135 project_->code_folder) == options.include_paths.end()) {
2136 options.include_paths.push_back(project_->code_folder);
2137 }
2138 if (!z3dk.rom_path.empty()) {
2139 options.hooks_rom_path = z3dk.rom_path;
2140 }
2141 if (!rom_->filename().empty()) {
2142 options.hooks_rom_path = rom_->filename();
2143 }
2144 patch_manager_.SetZ3dkAssembleOptions(options);
2145 }
2146#endif
2148 if (!status.ok()) {
2149 LOG_ERROR("Settings", "Failed to apply patches: %s", status.message());
2150 } else {
2151 LOG_INFO("Settings", "Applied %d patches successfully", enabled_count);
2152 }
2153 } else {
2154 LOG_WARN("Settings", "No ROM loaded");
2155 }
2156 }
2157 if (ImGui::IsItemHovered()) {
2158 ImGui::SetTooltip(tr("Apply all enabled patches to the loaded ROM"));
2159 }
2160
2161 ImGui::SameLine();
2162 if (ImGui::Button(ICON_MD_SAVE " Save All")) {
2163 auto status = patch_manager_.SaveAllPatches();
2164 if (!status.ok()) {
2165 LOG_ERROR("Settings", "Failed to save patches: %s", status.message());
2166 }
2167 }
2168
2169 if (ImGui::Button(ICON_MD_REFRESH " Reload Patches")) {
2170 patches_loaded_ = false;
2171 selected_patch_ = nullptr;
2172 }
2173}
2174
2175void SettingsPanel::DrawPatchList(const std::string& folder) {
2176 auto patches = patch_manager_.GetPatchesInFolder(folder);
2177
2178 if (patches.empty()) {
2179 ImGui::TextDisabled(tr("No patches in this folder"));
2180 return;
2181 }
2182
2183 // Use a child region for scrolling
2184 float available_height = std::min(200.0f, patches.size() * 25.0f + 10.0f);
2185 if (ImGui::BeginChild("##PatchList", ImVec2(0, available_height), true)) {
2186 for (auto* patch : patches) {
2187 ImGui::PushID(patch->filename().c_str());
2188
2189 bool enabled = patch->enabled();
2190 if (ImGui::Checkbox("##Enabled", &enabled)) {
2191 patch->set_enabled(enabled);
2192 }
2193
2194 ImGui::SameLine();
2195
2196 // Highlight selected patch
2197 bool is_selected = (selected_patch_ == patch);
2198 if (ImGui::Selectable(patch->name().c_str(), is_selected)) {
2199 selected_patch_ = patch;
2200 }
2201
2202 ImGui::PopID();
2203 }
2204 }
2205 ImGui::EndChild();
2206}
2207
2209 if (!selected_patch_)
2210 return;
2211
2212 ImGui::Text("%s %s", ICON_MD_INFO, selected_patch_->name().c_str());
2213
2214 if (!selected_patch_->author().empty()) {
2215 ImGui::TextDisabled(tr("by %s"), selected_patch_->author().c_str());
2216 }
2217
2218 if (!selected_patch_->version().empty()) {
2219 ImGui::SameLine();
2220 ImGui::TextDisabled(tr("v%s"), selected_patch_->version().c_str());
2221 }
2222
2223 // Description
2224 if (!selected_patch_->description().empty()) {
2225 ImGui::Spacing();
2226 ImGui::TextWrapped("%s", selected_patch_->description().c_str());
2227 }
2228
2229 // Parameters
2230 auto& params = selected_patch_->mutable_parameters();
2231 if (!params.empty()) {
2232 ImGui::Spacing();
2233 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
2234 ImGui::Separator();
2235
2236 for (auto& param : params) {
2237 DrawParameterWidget(&param);
2238 }
2239 }
2240}
2241
2243 if (!param)
2244 return;
2245
2246 ImGui::PushID(param->define_name.c_str());
2247
2248 switch (param->type) {
2252 int value = param->value;
2253 const char* format = param->use_decimal ? "%d" : "$%X";
2254
2255 ImGui::Text("%s", param->display_name.c_str());
2256 ImGui::SetNextItemWidth(100);
2257 if (ImGui::InputInt("##Value", &value, 1, 16)) {
2258 param->value = std::clamp(value, param->min_value, param->max_value);
2259 }
2260
2261 // Show range hint
2262 if (param->min_value != 0 || param->max_value != 0xFF) {
2263 ImGui::SameLine();
2264 ImGui::TextDisabled("(%d-%d)", param->min_value, param->max_value);
2265 }
2266 break;
2267 }
2268
2270 bool checked = (param->value == param->checked_value);
2271 if (ImGui::Checkbox(param->display_name.c_str(), &checked)) {
2272 param->value = checked ? param->checked_value : param->unchecked_value;
2273 }
2274 break;
2275 }
2276
2278 ImGui::Text("%s", param->display_name.c_str());
2279 for (size_t i = 0; i < param->choices.size(); ++i) {
2280 bool selected = (param->value == static_cast<int>(i));
2281 if (ImGui::RadioButton(param->choices[i].c_str(), selected)) {
2282 param->value = static_cast<int>(i);
2283 }
2284 }
2285 break;
2286 }
2287
2289 ImGui::Text("%s", param->display_name.c_str());
2290 for (size_t i = 0; i < param->choices.size(); ++i) {
2291 if (param->choices[i].empty() || param->choices[i] == "_EMPTY") {
2292 continue;
2293 }
2294 bool bit_set = (param->value & (1 << i)) != 0;
2295 if (ImGui::Checkbox(param->choices[i].c_str(), &bit_set)) {
2296 if (bit_set) {
2297 param->value |= (1 << i);
2298 } else {
2299 param->value &= ~(1 << i);
2300 }
2301 }
2302 }
2303 break;
2304 }
2305
2307 ImGui::Text("%s", param->display_name.c_str());
2308 // TODO: Implement item dropdown using game item names
2309 ImGui::SetNextItemWidth(150);
2310 if (ImGui::InputInt(tr("Item ID"), &param->value)) {
2311 param->value = std::clamp(param->value, 0, 255);
2312 }
2313 break;
2314 }
2315 }
2316
2317 ImGui::PopID();
2318 ImGui::Spacing();
2319}
2320
2321} // namespace editor
2322} // namespace yaze
auto filename() const
Definition rom.h:157
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool is_loaded() const
Definition rom.h:144
const std::string & version() const
Definition asm_patch.h:86
std::vector< PatchParameter > & mutable_parameters()
Definition asm_patch.h:98
const std::string & author() const
Definition asm_patch.h:85
const std::string & description() const
Definition asm_patch.h:87
const std::string & name() const
Definition asm_patch.h:84
const std::vector< FeatureFlag > & feature_flags() const
const MessageLayout & message_layout() const
const std::vector< RoomTagEntry > & room_tags() const
Get all room tags.
const std::string & hack_name() const
bool loaded() const
Check if the manifest has been loaded.
const BuildPipeline & build_pipeline() const
absl::Status ApplyEnabledPatches(Rom *rom)
Apply all enabled patches to a ROM.
absl::Status SaveAllPatches()
Save all patches to their files.
const std::vector< std::string > & folders() const
Get list of patch folder names.
int GetEnabledPatchCount() const
Get count of enabled patches.
std::vector< AsmPatch * > GetPatchesInFolder(const std::string &folder)
Get all patches in a specific folder.
const std::vector< std::unique_ptr< AsmPatch > > & patches() const
Get all loaded patches.
absl::Status LoadPatches(const std::string &patches_dir)
Load all patches from a directory structure.
virtual void SetDependencies(const EditorDependencies &deps)
Definition editor.h:250
Manages ImGui DockBuilder layouts for each editor type.
absl::Status ApplyDockTree(const layout_designer::DockTree &tree, ImGuiID dockspace_id)
Apply a DockTree to the given dockspace.
ImGuiID GetMainDockspaceId() const
Get the cached main dockspace ID.
void SetStatusBar(StatusBar *bar)
void DrawPatchList(const std::string &folder)
void SetWindowManager(WorkspaceWindowManager *registry)
ShortcutManager * shortcut_manager_
void SetDependencies(const EditorDependencies &deps) override
void ApplyNamedLayoutToDockspace(const std::string &name)
bool MatchesShortcutFilter(const std::string &text) const
void DrawParameterWidget(core::PatchParameter *param)
core::AsmPatch * selected_patch_
core::PatchManager patch_manager_
project::YazeProject * project_
void SetShortcutManager(ShortcutManager *manager)
void SetUserSettings(UserSettings *settings)
void SetProject(project::YazeProject *project)
WorkspaceWindowManager * window_manager_
std::vector< Shortcut > GetShortcutsByScope(Shortcut::Scope scope) const
bool UpdateShortcutKeys(const std::string &name, const std::vector< ImGuiKey > &keys)
void SetEnabled(bool enabled)
Enable or disable the status bar.
Definition status_bar.h:68
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
std::vector< std::string > GetAllCategories(size_t session_id) const
static MotionProfile ClampMotionProfile(int raw_profile)
Definition animator.cc:110
void SetMotionPreferences(bool reduced_motion, MotionProfile profile)
Definition animator.cc:120
static ThemeManager & Get()
static std::string ShowOpenFolderDialog()
ShowOpenFolderDialog opens a file dialog and returns the selected folder path. Uses global feature fl...
static absl::StatusOr< std::filesystem::path > GetUserDocumentsSubdirectory(const std::string &subdir)
Get a subdirectory within the user documents folder.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsDirectory()
Get the user's Documents directory.
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
static std::string NormalizePathForDisplay(const std::filesystem::path &path)
Normalize path separators for display.
static std::filesystem::path GetHomeDirectory()
Get the user's home directory in a cross-platform way.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_STORAGE
Definition icons.h:1865
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_TRAIN
Definition icons.h:2005
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_TEXT_FIELDS
Definition icons.h:1954
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_SPEED
Definition icons.h:1817
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_HOME
Definition icons.h:953
#define ICON_MD_EXTENSION
Definition icons.h:715
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_DASHBOARD_CUSTOMIZE
Definition icons.h:518
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_PSYCHOLOGY
Definition icons.h:1523
#define ICON_MD_BOLT
Definition icons.h:282
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_FLAG
Definition icons.h:784
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_HORIZONTAL_RULE
Definition icons.h:960
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_BACKUP
Definition icons.h:231
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_SYNC
Definition icons.h:1919
#define ICON_MD_CLOUD
Definition icons.h:423
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_VPN_KEY
Definition icons.h:2113
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define ICON_MD_HISTORY
Definition icons.h:946
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
constexpr char kProviderOpenAi[]
constexpr char kProviderOllama[]
Definition provider_ids.h:8
constexpr char kProviderLmStudio[]
LayoutManager * layout_manager()
Get the shared LayoutManager instance.
std::string ExpandLeadingTilde(const std::string &path)
bool ParseHexToken(const std::string &token, uint16_t *out)
bool AddUniquePath(std::vector< std::string > *paths, const std::string &path)
std::string BuildHostTagString(const UserSettings::Preferences::AiHost &host)
absl::StatusOr< DockTree > DockTreeFromJson(const nlohmann::json &j)
std::vector< ImGuiKey > ParseShortcut(const std::string &shortcut)
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
bool DrawProperty(const char *label, bool *value, const PropertyOptions &opts)
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
DensityPreset
Typography and spacing density presets.
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
Animator & GetAnimator()
Definition animator.cc:318
bool FontPicker(const char *label, int *index)
std::string ComputeRomHash(const uint8_t *data, size_t size)
Definition rom_hash.cc:70
void SetPreferHmagicSpriteNames(bool prefer)
Definition sprite.cc:273
void SetActiveFontIndex(int index)
Represents a configurable parameter within an ASM patch.
Definition asm_patch.h:33
PatchParameterType type
Definition asm_patch.h:36
std::vector< std::string > choices
Definition asm_patch.h:43
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
std::vector< std::string > include_paths
std::vector< std::pair< std::string, std::string > > defines
Unified dependency container for all editor types.
Definition editor.h:169
project::YazeProject * project
Definition editor.h:173
ShortcutManager * shortcut_manager
Definition editor.h:185
WorkspaceWindowManager * window_manager
Definition editor.h:181
std::vector< std::string > project_root_paths
std::unordered_map< std::string, std::string > panel_shortcuts
std::unordered_map< std::string, std::string > named_layouts
std::unordered_map< std::string, std::string > editor_shortcuts
std::unordered_map< std::string, std::string > global_shortcuts
void DrawZSCustomOverworldFlags(Rom *rom)
Comprehensive theme structure for YAZE.
std::vector< uint16_t > track_object_ids
Definition project.h:100
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:101
std::vector< uint16_t > track_stop_tiles
Definition project.h:96
std::vector< uint16_t > track_tiles
Definition project.h:95
std::vector< uint16_t > track_switch_tiles
Definition project.h:97
std::string expected_hash
Definition project.h:109
RomWritePolicy write_policy
Definition project.h:110
std::string rom_backup_folder
Definition project.h:181
std::string git_repository
Definition project.h:226
core::HackManifest hack_manifest
Definition project.h:212
std::string hack_manifest_file
Definition project.h:194
WorkspaceSettings workspace_settings
Definition project.h:201
std::string output_folder
Definition project.h:219
DungeonOverlaySettings dungeon_overlay
Definition project.h:202
std::string symbols_filename
Definition project.h:190
Z3dkSettings z3dk_settings
Definition project.h:215