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
4#include "util/i18n/tr.h"
5
6#include <algorithm>
7#include <cstring>
8#include <filesystem>
9#include <set>
10#include <vector>
11
12#include "absl/strings/ascii.h"
13#include "absl/strings/match.h"
14#include "absl/strings/str_cat.h"
15#include "absl/strings/str_format.h"
24#include "app/gui/core/icons.h"
25#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
61std::string FormatHexList(const std::vector<uint16_t>& values) {
62 std::string result;
63 result.reserve(values.size() * 6);
64 for (size_t i = 0; i < values.size(); ++i) {
65 const uint16_t value = values[i];
66 std::string token = value <= 0xFF ? absl::StrFormat("0x%02X", value)
67 : absl::StrFormat("0x%04X", value);
68 if (!result.empty()) {
69 result.append(", ");
70 }
71 result.append(token);
72 }
73 return result;
74}
75
76std::vector<uint16_t> DefaultTrackTiles() {
77 std::vector<uint16_t> values;
78 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
79 values.push_back(tile);
80 }
81 return values;
82}
83
84std::vector<uint16_t> DefaultStopTiles() {
85 return {0xB7, 0xB8, 0xB9, 0xBA};
86}
87
88std::vector<uint16_t> DefaultSwitchTiles() {
89 return {0xD0, 0xD1, 0xD2, 0xD3};
90}
91
92std::vector<uint16_t> DefaultTrackObjectIds() {
93 return {0x31};
94}
95
96std::vector<uint16_t> DefaultMinecartSpriteIds() {
97 return {0xA3};
98}
99
100bool IsLocalEndpoint(const std::string& base_url) {
101 if (base_url.empty()) {
102 return false;
103 }
104 std::string lower = absl::AsciiStrToLower(base_url);
105 return absl::StrContains(lower, "localhost") ||
106 absl::StrContains(lower, "127.0.0.1") ||
107 absl::StrContains(lower, "::1") ||
108 absl::StrContains(lower, "0.0.0.0") ||
109 absl::StrContains(lower, "192.168.") || absl::StartsWith(lower, "10.");
110}
111
112bool IsTailscaleEndpoint(const std::string& base_url) {
113 if (base_url.empty()) {
114 return false;
115 }
116 std::string lower = absl::AsciiStrToLower(base_url);
117 return absl::StrContains(lower, ".ts.net") ||
118 absl::StrContains(lower, "100.64.");
119}
120
122 std::vector<std::string> tags;
123 if (IsLocalEndpoint(host.base_url)) {
124 tags.push_back("local");
125 }
126 if (IsTailscaleEndpoint(host.base_url)) {
127 tags.push_back("tailscale");
128 }
129 if (absl::StartsWith(absl::AsciiStrToLower(host.base_url), "https://")) {
130 tags.push_back("https");
131 } else if (absl::StartsWith(absl::AsciiStrToLower(host.base_url),
132 "http://") &&
133 !IsLocalEndpoint(host.base_url) &&
134 !IsTailscaleEndpoint(host.base_url)) {
135 tags.push_back("http");
136 }
137 if (host.supports_vision) {
138 tags.push_back("vision");
139 }
140 if (host.supports_tools) {
141 tags.push_back("tools");
142 }
143 if (host.supports_streaming) {
144 tags.push_back("stream");
145 }
146 if (tags.empty()) {
147 return "";
148 }
149 std::string result = "[";
150 for (size_t i = 0; i < tags.size(); ++i) {
151 result += tags[i];
152 if (i + 1 < tags.size()) {
153 result += ", ";
154 }
155 }
156 result += "]";
157 return result;
158}
159
160// Expands a leading '~' or '~/' to the user's home directory so paths entered
161// by the user (or copied from other platforms) resolve on Linux/Windows.
162std::string ExpandLeadingTilde(const std::string& path) {
163 if (path.empty() || path.front() != '~') {
164 return path;
165 }
166 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
167 if (home_dir.empty() || home_dir == ".") {
168 return path;
169 }
170 if (path.size() == 1) {
171 return home_dir.string();
172 }
173 if (path[1] == '/' || path[1] == '\\') {
174 return (home_dir / path.substr(2)).string();
175 }
176 return (home_dir / path.substr(1)).string();
177}
178
179bool AddUniquePath(std::vector<std::string>* paths, const std::string& path) {
180 if (!paths || path.empty()) {
181 return false;
182 }
183 const std::string expanded = ExpandLeadingTilde(path);
184 auto it = std::find(paths->begin(), paths->end(), expanded);
185 if (it != paths->end()) {
186 return false;
187 }
188 paths->push_back(expanded);
189 return true;
190}
191
192constexpr std::array<SettingsPanel::CategoryDescriptor, 10>
194 {"appearance", "Appearance", ICON_MD_PALETTE,
196 "theme font scale density motion colors"},
197 {"behavior", "Editor Behavior", ICON_MD_TUNE,
199 "autosave backup tips editor defaults"},
200 {"performance", "Performance", ICON_MD_SPEED,
202 "vsync fps cache undo performance"},
203 {"files", "Files & Sync", ICON_MD_STORAGE,
205 "folders roots filesystem icloud sync"},
206 {"ai", "AI Agent", ICON_MD_SMART_TOY,
208 "agent provider model host ollama gemini openai"},
209 {"shortcuts", "Keyboard Shortcuts", ICON_MD_KEYBOARD,
210 SettingsPanel::Scope::kApplication, "keys bindings commands panels"},
211 {"layout", "Workspace Layout", ICON_MD_DASHBOARD_CUSTOMIZE,
213 "layout dock windows named workspace"},
214 {"project", "Project Configuration", ICON_MD_FOLDER,
216 "project rom identity paths build manifest"},
217 {"features", "Feature Flags", ICON_MD_FLAG,
219 "experimental system overworld dungeon resources save flags"},
220 {"patches", "ASM Patches", ICON_MD_EXTENSION,
221 SettingsPanel::Scope::kProject, "asar asm patches parameters"},
222 }};
223
225 switch (scope) {
227 return "Application";
229 return "Workspace";
231 return "Project";
232 }
233 return "Application";
234}
235
236} // namespace
237
238std::vector<SettingsPanel::CategoryDescriptor> SettingsPanel::FilterCategories(
239 Scope scope, const std::string& query) {
240 const std::string needle = absl::AsciiStrToLower(query);
241 std::vector<CategoryDescriptor> result;
242 for (const CategoryDescriptor& category : kSettingsCategories) {
243 if (category.scope != scope) {
244 continue;
245 }
246 if (!needle.empty()) {
247 const std::string haystack = absl::AsciiStrToLower(absl::StrFormat(
248 "%s %s %s", category.id, category.label, category.keywords));
249 if (haystack.find(needle) == std::string::npos) {
250 continue;
251 }
252 }
253 result.push_back(category);
254 }
255 return result;
256}
257
259 const project::DungeonOverlaySettings& overlay) {
260 const auto summarize = [](const std::vector<uint16_t>& configured,
261 const std::vector<uint16_t>& standard) {
262 const bool uses_standard_values =
263 configured.empty() || configured == standard;
264 return std::pair<std::string, bool>(
265 FormatHexList(configured.empty() ? standard : configured),
266 uses_standard_values);
267 };
268
269 return {summarize(overlay.track_tiles, DefaultTrackTiles()),
270 summarize(overlay.track_stop_tiles, DefaultStopTiles()),
271 summarize(overlay.track_switch_tiles, DefaultSwitchTiles()),
272 summarize(overlay.track_object_ids, DefaultTrackObjectIds()),
273 summarize(overlay.minecart_sprite_ids, DefaultMinecartSpriteIds())};
274}
275
278 project_status_message_ = "Minecart Tracks navigation is unavailable.";
279 return absl::FailedPreconditionError(project_status_message_);
280 }
281
282 const absl::Status status = open_minecart_tracks_callback_();
283 if (!status.ok()) {
284 project_status_message_ = std::string(status.message());
285 return status;
286 }
287
289 return absl::OkStatus();
290}
291
293 if (project_provider_) {
295 }
296 if (!user_settings_) {
297 ImGui::TextDisabled(tr("Settings not available"));
298 return;
299 }
300
302 ImGui::SetNextItemWidth(-1.0f);
303 ImGui::InputTextWithHint("##SettingsSearch",
304 ICON_MD_SEARCH " Search settings",
305 settings_search_.data(), settings_search_.size());
306 gui::WidgetIdRegistry::Instance().RegisterWidget("Settings/input:Search",
307 "input", ImGui::GetItemID());
308
309 const auto categories =
311 EnsureActiveCategory(categories);
312 if (categories.empty()) {
313 ImGui::Spacing();
314 ImGui::TextDisabled(tr("No %s settings match \"%s\"."),
315 ScopeLabel(active_scope_), settings_search_.data());
316 return;
317 }
318
319 ImGui::Spacing();
320 const bool wide_layout =
321 ImGui::GetContentRegionAvail().x >= gui::ScaledSize(520.0f, 0.0f).x;
322 if (wide_layout && ImGui::BeginTable("##SettingsLayout", 2,
323 ImGuiTableFlags_Resizable |
324 ImGuiTableFlags_BordersInnerV |
325 ImGuiTableFlags_SizingStretchProp)) {
326 ImGui::TableSetupColumn("Categories", ImGuiTableColumnFlags_WidthFixed,
327 std::clamp(ImGui::GetContentRegionAvail().x * 0.30f,
328 gui::ScaledSize(150.0f, 0.0f).x,
329 gui::ScaledSize(220.0f, 0.0f).x));
330 ImGui::TableSetupColumn("Settings", ImGuiTableColumnFlags_WidthStretch);
331 ImGui::TableNextRow();
332 ImGui::TableNextColumn();
333 DrawCategoryNavigation(categories, false);
334 ImGui::TableNextColumn();
336 ImGui::EndTable();
337 return;
338 }
339
340 DrawCategoryNavigation(categories, true);
342}
343
345 if (!ImGui::BeginTabBar("##SettingsScopes")) {
346 return;
347 }
348 for (const Scope scope :
350 const bool selected = active_scope_ == scope;
351 const bool tab_open = ImGui::BeginTabItem(ScopeLabel(scope));
353 std::string("Settings/tab:") + ScopeLabel(scope), "tab",
354 ImGui::GetItemID());
355 if (tab_open) {
356 if (!selected) {
357 active_scope_ = scope;
358 active_category_.clear();
359 settings_search_.fill('\0');
360 }
361 ImGui::EndTabItem();
362 }
363 }
364 ImGui::EndTabBar();
365}
366
368 const std::vector<CategoryDescriptor>& categories) {
369 if (categories.empty()) {
370 active_category_.clear();
371 return;
372 }
373 const auto active = std::find_if(categories.begin(), categories.end(),
374 [this](const CategoryDescriptor& category) {
375 return active_category_ == category.id;
376 });
377 if (active == categories.end()) {
378 active_category_ = categories.front().id;
379 }
380}
381
383 const std::vector<CategoryDescriptor>& categories, bool compact) {
384 if (compact) {
385 const CategoryDescriptor* active = nullptr;
386 for (const CategoryDescriptor& category : categories) {
387 if (active_category_ == category.id) {
388 active = &category;
389 break;
390 }
391 }
392 const std::string preview =
393 active ? absl::StrFormat("%s %s", active->icon, active->label)
394 : std::string("Select category");
395 ImGui::SetNextItemWidth(-1.0f);
396 if (ImGui::BeginCombo("##SettingsCategory", preview.c_str())) {
397 for (const CategoryDescriptor& category : categories) {
398 const bool selected = active_category_ == category.id;
399 const std::string label =
400 absl::StrFormat("%s %s", category.icon, category.label);
401 if (ImGui::Selectable(label.c_str(), selected)) {
402 active_category_ = category.id;
403 }
404 if (selected) {
405 ImGui::SetItemDefaultFocus();
406 }
407 }
408 ImGui::EndCombo();
409 }
410 return;
411 }
412
413 const bool nav_open =
414 ImGui::BeginChild("##SettingsCategories", ImVec2(0.0f, 0.0f), false);
415 if (nav_open) {
416 for (const CategoryDescriptor& category : categories) {
417 const bool selected = active_category_ == category.id;
418 const std::string label =
419 absl::StrFormat("%s %s", category.icon, category.label);
420 if (ImGui::Selectable(label.c_str(), selected)) {
421 active_category_ = category.id;
422 }
423 }
424 }
425 ImGui::EndChild();
426}
427
428void SettingsPanel::DrawCategoryContent(const std::string& category_id) {
429 const auto descriptor =
430 std::find_if(kSettingsCategories.begin(), kSettingsCategories.end(),
431 [&category_id](const CategoryDescriptor& category) {
432 return category_id == category.id;
433 });
434 if (descriptor == kSettingsCategories.end()) {
435 return;
436 }
437
438 const std::string heading =
439 absl::StrFormat("%s %s", descriptor->icon, descriptor->label);
440 ImGui::SeparatorText(heading.c_str());
441 if (category_id == "appearance") {
443 } else if (category_id == "behavior") {
445 } else if (category_id == "performance") {
447 } else if (category_id == "files") {
449 } else if (category_id == "ai") {
451 } else if (category_id == "shortcuts") {
453 } else if (category_id == "layout") {
455 } else if (category_id == "project") {
457 } else if (category_id == "features") {
459 } else if (category_id == "patches") {
461 }
462}
463
465 // Refactored from table to vertical list for sidebar
466 static gui::FlagsMenu flags;
467
468 ImGui::TextDisabled(tr("Feature Flags configuration"));
469 ImGui::Spacing();
470
471 bool experiments_enabled = user_settings_->prefs().show_experimental_editors;
472 if (ImGui::Checkbox(tr("Acknowledge Experimental Editors"),
473 &experiments_enabled)) {
474 user_settings_->prefs().show_experimental_editors = experiments_enabled;
476 }
477 if (ImGui::IsItemHovered()) {
478 ImGui::SetTooltip(
479 tr("Acknowledges Screen, Music, and Agent editor maturity. Turning "
480 "this off keeps the editors visible but warns and may force a "
481 "backup. Feature-specific write guards remain authoritative."));
482 }
483 if (!experiments_enabled) {
484 ImGui::TextDisabled(
485 "%s",
486 tr("Experimental editors remain available with warnings and defensive "
487 "save safeguards."));
488 }
489 ImGui::Separator();
490
491 if (ImGui::TreeNode(ICON_MD_FLAG " System Flags")) {
492 flags.DrawSystemFlags();
493 ImGui::TreePop();
494 }
495
496 if (ImGui::TreeNode(ICON_MD_MAP " Overworld Flags")) {
497 flags.DrawOverworldFlags();
498 ImGui::TreePop();
499 }
500
501 if (ImGui::TreeNode(ICON_MD_EXTENSION " ZSCustomOverworld Enable Flags")) {
503 ImGui::TreePop();
504 }
505
506 if (ImGui::TreeNode(ICON_MD_CASTLE " Dungeon Flags")) {
507 flags.DrawDungeonFlags();
508 ImGui::TreePop();
509 }
510
511 if (ImGui::TreeNode(ICON_MD_FOLDER_SPECIAL " Resource Flags")) {
512 flags.DrawResourceFlags();
513 ImGui::TreePop();
514 }
515}
516
518 if (!project_) {
519 ImGui::TextDisabled(tr("No active project."));
520 return;
521 }
522
523 ImGui::Text(tr("%s Project Info"), ICON_MD_INFO);
524 ImGui::Separator();
525
526 ImGui::Text(tr("Name: %s"), project_->name.c_str());
527 ImGui::Text(tr("Path: %s"), project_->filepath.c_str());
528
529 ImGui::Spacing();
530 ImGui::Text(tr("%s ROM Identity"), ICON_MD_VIDEOGAME_ASSET);
531 ImGui::Separator();
532
533 const char* roles[] = {"base", "dev", "patched", "release"};
534 int role_index = static_cast<int>(project_->rom_metadata.role);
535 if (ImGui::Combo(tr("Role"), &role_index, roles, IM_ARRAYSIZE(roles))) {
536 project_->rom_metadata.role = static_cast<project::RomRole>(role_index);
537 project_->Save();
538 }
539
540 const char* policies[] = {"allow", "warn", "block"};
541 int policy_index = static_cast<int>(project_->rom_metadata.write_policy);
542 if (ImGui::Combo(tr("Write Policy"), &policy_index, policies,
543 IM_ARRAYSIZE(policies))) {
545 static_cast<project::RomWritePolicy>(policy_index);
546 project_->Save();
547 }
548
549 std::string expected_hash = project_->rom_metadata.expected_hash;
550 if (ImGui::InputText(tr("Expected Hash"), &expected_hash)) {
551 project_->rom_metadata.expected_hash = expected_hash;
552 project_->Save();
553 }
554
555 if (rom_ && rom_->is_loaded()) {
556 // Use the identity checked by save policy, not a separately cached hash of
557 // the editor's mutable ROM buffer (which may already contain edits).
558 const auto current_hash =
561 ImGui::Text(tr("Current ROM Hash: %s"),
562 current_hash.empty() ? "(unknown)" : current_hash.c_str());
563 const bool use_hash = ImGui::Button(tr("Use Current ROM Hash"));
565 "Settings/button:Use Current ROM Hash", "button", ImGui::GetItemID());
566 if (use_hash && !current_hash.empty()) {
567 project_->rom_metadata.expected_hash = current_hash;
568 project_->Save();
569 }
570 } else {
571 ImGui::TextDisabled(tr("Current ROM Hash: (no ROM loaded)"));
572 }
573
574 ImGui::Spacing();
575 ImGui::Text(tr("%s Paths"), ICON_MD_FOLDER_OPEN);
576 ImGui::Separator();
577
578 // Output Folder
579 std::string output_folder = project_->output_folder;
580 if (ImGui::InputText(tr("Output Folder"), &output_folder)) {
581 project_->output_folder = output_folder;
582 project_->Save();
583 }
584
585 // Git Repository
586 std::string git_repo = project_->git_repository;
587 if (ImGui::InputText(tr("Git Repository"), &git_repo)) {
588 project_->git_repository = git_repo;
589 project_->Save();
590 }
591
592 ImGui::Spacing();
593 ImGui::Text(tr("%s Build"), ICON_MD_BUILD);
594 ImGui::Separator();
595
596 // Build Target
597 std::string build_target = project_->build_target;
598 if (ImGui::InputText(tr("Build Target (ROM)"), &build_target)) {
599 project_->build_target = build_target;
600 project_->Save();
601 }
602
603 // Symbols File
604 std::string symbols_file = project_->symbols_filename;
605 if (ImGui::InputText(tr("Symbols File"), &symbols_file)) {
606 project_->symbols_filename = symbols_file;
607 project_->Save();
608 }
609
610 ImGui::Spacing();
611 ImGui::Text(tr("%s ASM / Hack Manifest"), ICON_MD_CODE);
612 ImGui::Separator();
613 ImGui::TextWrapped(tr(
614 "Optional: load a hack manifest JSON (generated by an ASM project) to "
615 "annotate room tags, show feature flags, and surface which ROM regions "
616 "are owned by ASM vs safe to edit in yaze."));
617
618 std::string manifest_file = project_->hack_manifest_file;
619 if (ImGui::InputText(tr("Hack Manifest File"), &manifest_file)) {
620 project_->hack_manifest_file = manifest_file;
622 project_->Save();
623 }
624
625 const bool manifest_loaded = project_->hack_manifest.loaded();
626 ImGui::SameLine();
627 ImGui::TextDisabled(manifest_loaded ? "(loaded)" : "(not loaded)");
628 if (ImGui::Button(tr("Reload Manifest"))) {
630 }
631
632 if (manifest_loaded) {
633 ImGui::Spacing();
634 ImGui::Text(tr("Hack: %s"), project_->hack_manifest.hack_name().c_str());
635 ImGui::Text(tr("Manifest Version: %d"),
637 ImGui::Text(tr("Hooks Tracked: %d"), project_->hack_manifest.total_hooks());
638
639 const auto& pipeline = project_->hack_manifest.build_pipeline();
640 if (!pipeline.dev_rom.empty()) {
641 ImGui::Text(tr("Dev ROM: %s"), pipeline.dev_rom.c_str());
642 }
643 if (!pipeline.patched_rom.empty()) {
644 ImGui::Text(tr("Patched ROM: %s"), pipeline.patched_rom.c_str());
645 }
646 if (!pipeline.build_script.empty()) {
647 ImGui::Text(tr("Build Script: %s"), pipeline.build_script.c_str());
648 }
649
650 const auto& msg_layout = project_->hack_manifest.message_layout();
651 if (msg_layout.first_expanded_id != 0 || msg_layout.last_expanded_id != 0) {
652 ImGui::Text(tr("Expanded Messages: 0x%03X-0x%03X (%d)"),
653 msg_layout.first_expanded_id, msg_layout.last_expanded_id,
654 msg_layout.expanded_count);
655 }
656
657 if (ImGui::TreeNode(ICON_MD_FLAG " Hack Feature Flags")) {
658 for (const auto& flag : project_->hack_manifest.feature_flags()) {
659 ImGui::BulletText("%s = %d (%s)", flag.name.c_str(), flag.value,
660 flag.enabled ? "enabled" : "disabled");
661 if (!flag.source.empty()) {
662 ImGui::SameLine();
663 ImGui::TextDisabled("%s", flag.source.c_str());
664 }
665 }
666 ImGui::TreePop();
667 }
668
669 if (ImGui::TreeNode(ICON_MD_LABEL " Room Tags (Dispatch)")) {
670 for (const auto& tag : project_->hack_manifest.room_tags()) {
671 ImGui::BulletText(tr("0x%02X: %s"), tag.tag_id, tag.name.c_str());
672 if (!tag.enabled && !tag.feature_flag.empty()) {
673 ImGui::SameLine();
674 ImGui::TextDisabled(tr("(disabled by %s)"), tag.feature_flag.c_str());
675 }
676 if (!tag.purpose.empty() && ImGui::IsItemHovered()) {
677 ImGui::SetTooltip("%s", tag.purpose.c_str());
678 }
679 }
680 ImGui::TreePop();
681 }
682 }
683
684 ImGui::Spacing();
685 ImGui::Text(tr("%s Backup Settings"), ICON_MD_BACKUP);
686 ImGui::Separator();
687
688 std::string backup_folder = project_->rom_backup_folder;
689 if (ImGui::InputText(tr("Backup Folder"), &backup_folder)) {
690 project_->rom_backup_folder = backup_folder;
691 project_->Save();
692 }
693
694 bool backup_on_save = project_->workspace_settings.backup_on_save;
695 if (gui::DrawProperty("Backup Before Save", &backup_on_save)) {
697 project_->Save();
698 }
699
701 if (gui::DrawProperty("Retention Count", &retention,
702 {.min = 0, .max = 10000})) {
704 project_->Save();
705 }
706
708 if (gui::DrawProperty("Keep Daily Snapshots", &keep_daily)) {
710 project_->Save();
711 }
712
714 if (gui::DrawProperty("Keep Daily Days", &keep_days,
715 {.min = 1, .max = 3650})) {
717 project_->Save();
718 }
719
720 ImGui::Spacing();
721 ImGui::Text(tr("%s Dungeon Overlay"), ICON_MD_TRAIN);
722 ImGui::Separator();
723 ImGui::TextWrapped(
724 tr("Read-only here. Edit collision and object IDs in Minecart Tracks > "
725 "Advanced."));
726
727 constexpr std::array<const char*, 5> kLabels = {
728 "Track Tiles", "Stop Tiles", "Switch Tiles", "Track Object IDs",
729 "Minecart Sprite IDs"};
730 const DungeonOverlaySummary summary =
732 if (ImGui::BeginTable("DungeonOverlaySummary", 2,
733 ImGuiTableFlags_SizingStretchProp)) {
734 ImGui::TableSetupColumn("Field", ImGuiTableColumnFlags_WidthFixed, 132.0f);
735 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
736 for (size_t i = 0; i < summary.size(); ++i) {
737 ImGui::TableNextRow();
738 ImGui::TableSetColumnIndex(0);
739 ImGui::TextDisabled("%s", tr(kLabels[i]));
740 ImGui::TableSetColumnIndex(1);
741 ImGui::TextWrapped("%s", summary[i].first.c_str());
742 ImGui::TextDisabled("%s", summary[i].second
743 ? tr("Standard values")
744 : tr("Custom project values"));
745 }
746 ImGui::EndTable();
747 }
748
749 ImGui::Spacing();
750 const bool custom_objects_enabled =
752 const bool can_open = custom_objects_enabled &&
753 static_cast<bool>(open_minecart_tracks_callback_);
754 ImGui::BeginDisabled(!can_open);
755 if (ImGui::Button(ICON_MD_TRAIN " Open Minecart Tracks")) {
757 }
758 ImGui::EndDisabled();
759
760 if (!custom_objects_enabled) {
761 ImGui::TextDisabled(
762 tr("Enable Custom Dungeon Objects in General Settings first."));
763 } else if (!open_minecart_tracks_callback_) {
764 ImGui::TextDisabled(tr("Minecart Tracks navigation is unavailable."));
765 }
766
767 if (!project_status_message_.empty()) {
768 ImGui::TextColored(gui::GetErrorColor(), "%s",
770 }
771}
772
774 if (!user_settings_) {
775 return;
776 }
777
778 auto& prefs = user_settings_->prefs();
779 auto& roots = prefs.project_root_paths;
780 static int selected_root_index = -1;
781 static std::string new_root_path;
782
783 ImGui::Text(tr("%s Project Roots"), ICON_MD_FOLDER_OPEN);
784 ImGui::Separator();
785
786 if (roots.empty()) {
787 ImGui::TextDisabled(tr("No project roots configured."));
788 }
789
790 const float roots_height = std::clamp(
791 ImGui::GetContentRegionAvail().y * 0.28f, gui::ScaledSize(0.0f, 100.0f).y,
792 gui::ScaledSize(0.0f, 220.0f).y);
793 if (ImGui::BeginChild("ProjectRootsList", ImVec2(0, roots_height), true)) {
794 for (size_t i = 0; i < roots.size(); ++i) {
795 const bool is_default = roots[i] == prefs.default_project_root;
796 std::string label =
798 if (is_default) {
799 label += " (default)";
800 }
801 if (ImGui::Selectable(label.c_str(),
802 selected_root_index == static_cast<int>(i))) {
803 selected_root_index = static_cast<int>(i);
804 }
805 }
806 }
807 ImGui::EndChild();
808
809 const bool has_selection =
810 selected_root_index >= 0 &&
811 selected_root_index < static_cast<int>(roots.size());
812 if (has_selection) {
813 if (ImGui::Button(tr("Set Default"))) {
814 prefs.default_project_root = roots[selected_root_index];
816 }
817 ImGui::SameLine();
818 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
819 const std::string removed = roots[selected_root_index];
820 roots.erase(roots.begin() + selected_root_index);
821 if (prefs.default_project_root == removed) {
822 prefs.default_project_root = roots.empty() ? "" : roots.front();
823 }
824 selected_root_index = roots.empty()
825 ? -1
826 : std::min(selected_root_index,
827 static_cast<int>(roots.size() - 1));
829 }
830 }
831
832 ImGui::Spacing();
833 ImGui::Text(tr("%s Add Root"), ICON_MD_ADD);
834 ImGui::Separator();
835
836 ImGui::InputTextWithHint("##project_root_add", "Add folder path...",
837 &new_root_path);
838 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
839 const std::string trimmed =
840 std::string(absl::StripAsciiWhitespace(new_root_path));
841 if (!trimmed.empty()) {
842 if (AddUniquePath(&roots, trimmed) &&
843 prefs.default_project_root.empty()) {
844 prefs.default_project_root = trimmed;
845 }
847 }
848 }
849 ImGui::SameLine();
850 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
851 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
852 if (!folder.empty()) {
853 if (AddUniquePath(&roots, folder) && prefs.default_project_root.empty()) {
854 prefs.default_project_root = folder;
855 }
857 }
858 }
859
860 ImGui::Spacing();
861 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
862 ImGui::Separator();
863
864 if (ImGui::Button(ICON_MD_HOME " Add Documents")) {
866 if (docs_dir.ok()) {
867 if (AddUniquePath(&roots, docs_dir->string()) &&
868 prefs.default_project_root.empty()) {
869 prefs.default_project_root = docs_dir->string();
870 }
872 }
873 }
874 ImGui::SameLine();
875 if (ImGui::Button(ICON_MD_CLOUD " Add iCloud Projects")) {
876 auto icloud_dir =
878 if (icloud_dir.ok()) {
879 if (AddUniquePath(&roots, icloud_dir->string())) {
880 prefs.default_project_root = icloud_dir->string();
881 }
883 }
884 }
885 ImGui::TextDisabled(
886 tr("iCloud projects live in Documents/Yaze/iCloud on this Mac."));
887
888 ImGui::Spacing();
889 ImGui::Text(tr("%s Sync Options"), ICON_MD_SYNC);
890 ImGui::Separator();
891
892 bool use_icloud_sync = prefs.use_icloud_sync;
893 if (ImGui::Checkbox(tr("Use iCloud sync (Documents)"), &use_icloud_sync)) {
894 prefs.use_icloud_sync = use_icloud_sync;
895 if (use_icloud_sync) {
896 auto icloud_dir =
898 if (icloud_dir.ok()) {
899 AddUniquePath(&roots, icloud_dir->string());
900 prefs.default_project_root = icloud_dir->string();
901 }
902 }
904 }
905
906 bool use_files_app = prefs.use_files_app;
907 if (ImGui::Checkbox(tr("Prefer Files app on iOS"), &use_files_app)) {
908 prefs.use_files_app = use_files_app;
910 }
911}
912
914 auto& theme_manager = gui::ThemeManager::Get();
915 auto theme = theme_manager.GetCurrentTheme();
916 theme.ApplyDensityPreset(preset);
917 // ReapplyTheme, not ApplyTheme: this re-applies the theme the user is
918 // already on, so Classic YAZE has to route back through ColorsYaze().
919 theme_manager.ReapplyTheme(theme);
920}
921
923 auto& theme_manager = gui::ThemeManager::Get();
924
925 ImGui::Text(tr("%s Theme"), ICON_MD_PALETTE);
926 ImGui::Separator();
927
928 const std::string current = theme_manager.GetCurrentThemeName();
929 const auto& current_theme = theme_manager.GetCurrentTheme();
930
931 ImGui::SetNextItemWidth(-1.0f);
932 if (ImGui::BeginCombo("##AppearanceTheme", current.c_str())) {
933 for (const auto& theme_name : theme_manager.GetAvailableThemes()) {
934 const bool is_current = theme_name == current;
935 if (ImGui::Selectable(theme_name.c_str(), is_current)) {
936 if (theme_manager.IsPreviewActive()) {
937 theme_manager.EndPreview();
938 }
939 theme_manager.ApplyTheme(theme_name);
940 }
941 if (is_current) {
942 ImGui::SetItemDefaultFocus();
943 }
944 }
945 ImGui::EndCombo();
946 }
947
948 const ImGuiColorEditFlags swatch_flags = ImGuiColorEditFlags_NoTooltip |
949 ImGuiColorEditFlags_NoDragDrop |
950 ImGuiColorEditFlags_NoPicker;
951 const float swatch_size = std::max(12.0f, ImGui::GetFrameHeight() * 0.62f);
952 ImGui::TextDisabled("%s", tr("Palette"));
953 ImGui::SameLine(0.0f, 6.0f);
954 ImGui::PushID("CurrentThemeSwatches");
955 ImGui::ColorButton("Primary",
956 gui::ConvertColorToImVec4(current_theme.primary),
957 swatch_flags, ImVec2(swatch_size, swatch_size));
958 ImGui::SameLine(0.0f, 3.0f);
959 ImGui::ColorButton("Surface",
960 gui::ConvertColorToImVec4(current_theme.surface),
961 swatch_flags, ImVec2(swatch_size, swatch_size));
962 ImGui::SameLine(0.0f, 3.0f);
963 ImGui::ColorButton("Accent", gui::ConvertColorToImVec4(current_theme.accent),
964 swatch_flags, ImVec2(swatch_size, swatch_size));
965 ImGui::PopID();
966 if (!current_theme.description.empty()) {
967 ImGui::PushTextWrapPos(0.0f);
968 ImGui::TextDisabled("%s", current_theme.description.c_str());
969 ImGui::PopTextWrapPos();
970 }
971
972 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload themes")) {
973 theme_manager.RefreshAvailableThemes();
974 }
975 if (ImGui::IsItemHovered()) {
976 ImGui::SetTooltip(tr("Re-scan theme folders for new or changed themes"));
977 }
978
979 ImGui::Spacing();
980 ImGui::SeparatorText(tr("Density"));
981
982 {
983 auto preset = theme_manager.GetCurrentTheme().density_preset;
984 int density = static_cast<int>(preset);
985 const char* density_labels[] = {tr("Compact"), tr("Normal"),
986 tr("Comfortable")};
987 ImGui::SetNextItemWidth(-1.0f);
988 if (ImGui::Combo("##DisplayDensity", &density, density_labels,
989 IM_ARRAYSIZE(density_labels))) {
990 ApplyDisplayDensity(static_cast<gui::DensityPreset>(density));
991 }
992 ImGui::TextDisabled(
993 "%s", density == 0 ? tr("Tighter controls and more visible content")
994 : density == 2 ? tr("Larger controls with more breathing room")
995 : tr("Balanced spacing for everyday editing"));
996 }
997
998 ImGui::Spacing();
999 ImGui::SeparatorText(tr("Motion"));
1000
1001 auto& prefs = user_settings_->prefs();
1002 bool reduced_motion = prefs.reduced_motion;
1003 if (ImGui::Checkbox(tr("Reduced Motion"), &reduced_motion)) {
1004 prefs.reduced_motion = reduced_motion;
1006 prefs.reduced_motion,
1007 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1009 }
1010 if (ImGui::IsItemHovered()) {
1011 ImGui::SetTooltip(
1012 tr("Disable panel/editor transition animations for a calmer editing "
1013 "experience."));
1014 }
1015
1016 int switch_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
1017 const char* switch_profile_labels[] = {"Snappy", "Standard", "Relaxed"};
1018 if (ImGui::Combo(tr("Switch Motion Profile"), &switch_profile,
1019 switch_profile_labels,
1020 IM_ARRAYSIZE(switch_profile_labels))) {
1021 prefs.switch_motion_profile = switch_profile;
1023 prefs.reduced_motion,
1024 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1026 }
1027 if (ImGui::IsItemHovered()) {
1028 ImGui::SetTooltip(
1029 tr("Controls editor/workspace switch timing and easing for panel fades "
1030 "and sidebar slides."));
1031 }
1032
1033 ImGui::Spacing();
1034 ImGui::Text(tr("%s Font"), ICON_MD_TEXT_FIELDS);
1035 ImGui::Separator();
1036
1037 if (user_settings_) {
1038 int font_index = user_settings_->prefs().font_family_index;
1039 if (gui::FontPicker("##font_family", &font_index)) {
1040 user_settings_->prefs().font_family_index = font_index;
1041 ::yaze::SetActiveFontIndex(font_index);
1043 }
1044
1045 ImGui::Text(tr("Global Font Scale"));
1046 float scale = user_settings_->prefs().font_global_scale;
1047 if (ImGui::SliderFloat("##global_font_scale", &scale, 0.5f, 2.0f, "%.2f")) {
1049 ImGui::GetIO().FontGlobalScale = scale;
1051 }
1052 }
1053
1054 ImGui::Spacing();
1055 ImGui::Text(tr("%s Status Bar"), ICON_MD_HORIZONTAL_RULE);
1056 ImGui::Separator();
1057
1058 bool show_status_bar = user_settings_->prefs().show_status_bar;
1059 if (ImGui::Checkbox(tr("Show Status Bar"), &show_status_bar)) {
1060 user_settings_->prefs().show_status_bar = show_status_bar;
1062 // Immediately apply to status bar if status_bar_ is available
1063 if (status_bar_) {
1064 status_bar_->SetEnabled(show_status_bar);
1065 }
1066 }
1067 if (ImGui::IsItemHovered()) {
1068 ImGui::SetTooltip(
1069 tr("Display ROM, session, cursor, and zoom info at bottom of window"));
1070 }
1071
1072 ImGui::Spacing();
1073 ImGui::Text(tr("%s Editor Behavior"), ICON_MD_TUNE);
1074 ImGui::Separator();
1075
1076 bool keep_emu_bg =
1078 if (ImGui::Checkbox(tr("Keep Emulator Running in Background"),
1079 &keep_emu_bg)) {
1082 }
1083 if (ImGui::IsItemHovered()) {
1084 ImGui::SetTooltip(tr(
1085 "When off (default), hiding emulator panels pauses the SNES tick and "
1086 "audio. Music playback still drives its own frames."));
1087 }
1088}
1089
1091 if (!user_settings_) {
1092 ImGui::TextDisabled(tr("UserSettings unavailable."));
1093 return;
1094 }
1095
1096 auto& prefs = user_settings_->prefs();
1097
1098 ImGui::Text(tr("%s Named Layouts"), ICON_MD_DASHBOARD);
1099 ImGui::Separator();
1100
1101 if (prefs.named_layouts.empty()) {
1102 ImGui::TextDisabled(
1103 tr("No saved layouts yet. Open the Layout Designer to capture one."));
1104 } else {
1105 // Sort the keys so the combo order is stable across frames
1106 // (named_layouts is unordered_map).
1107 std::vector<std::string> names;
1108 names.reserve(prefs.named_layouts.size());
1109 for (const auto& entry : prefs.named_layouts) {
1110 names.push_back(entry.first);
1111 }
1112 std::sort(names.begin(), names.end());
1113
1114 const std::string& active = prefs.last_applied_layout_name;
1115 const char* preview =
1116 active.empty() ? "(select a layout…)" : active.c_str();
1117
1118 if (ImGui::BeginCombo(tr("Active Layout"), preview)) {
1119 for (const auto& name : names) {
1120 const bool is_selected = (name == active);
1121 if (ImGui::Selectable(name.c_str(), is_selected)) {
1122 // Mirrors the LayoutManager startup-reapply path in shape:
1123 // lookup → parse → validate → ApplyDockTree. Inlined rather
1124 // than shared because the call sites have different error
1125 // surfacing — this one writes to a transient status string;
1126 // startup writes to the log and a one-shot consumed flag.
1128 }
1129 if (is_selected) {
1130 ImGui::SetItemDefaultFocus();
1131 }
1132 }
1133 ImGui::EndCombo();
1134 }
1135
1136 ImGui::SameLine();
1137 ImGui::BeginDisabled(active.empty() ||
1138 prefs.named_layouts.count(active) == 0);
1139 if (ImGui::SmallButton(tr("Re-apply"))) {
1141 }
1142 ImGui::EndDisabled();
1143 if (ImGui::IsItemHovered()) {
1144 ImGui::SetTooltip(
1145 tr("Re-apply the active layout (useful after closing or rearranging "
1146 "panels manually)."));
1147 }
1148 }
1149
1150 ImGui::Spacing();
1151 if (ImGui::Button(ICON_MD_DASHBOARD_CUSTOMIZE " Open Layout Designer")) {
1152 if (window_manager_ == nullptr) {
1153 workspace_status_message_ = "Designer unavailable: no window manager.";
1155 } else if (!window_manager_->IsWindowOpen("layout.designer") &&
1156 !window_manager_->OpenWindow("layout.designer")) {
1157 // OpenWindow returns false when the panel id isn't registered in
1158 // the active session. Surface that — silently doing nothing on
1159 // a missing registration would hide a real bug (panel renamed,
1160 // force-load archive misconfigured, etc.).
1162 "Open failed: \"layout.designer\" panel is not registered.";
1164 } else {
1165 window_manager_->SetWindowPinned("layout.designer", true);
1168 }
1169 }
1170 if (ImGui::IsItemHovered()) {
1171 ImGui::SetTooltip(
1172 tr("Open the Layout Designer panel to author or capture a workspace "
1173 "layout."));
1174 }
1175
1176 if (!workspace_status_message_.empty()) {
1177 ImGui::Spacing();
1179 ImGui::TextColored(gui::GetErrorColor(), "%s",
1181 } else {
1182 ImGui::TextDisabled("%s", workspace_status_message_.c_str());
1183 }
1184 }
1185}
1186
1187void SettingsPanel::ApplyNamedLayoutToDockspace(const std::string& name) {
1190 if (user_settings_ == nullptr) {
1191 workspace_status_message_ = "Apply failed: UserSettings unavailable.";
1193 return;
1194 }
1195 auto& prefs = user_settings_->prefs();
1196 const auto it = prefs.named_layouts.find(name);
1197 if (it == prefs.named_layouts.end()) {
1199 absl::StrCat("Apply failed: \"", name, "\" not found.");
1201 return;
1202 }
1203
1204 nlohmann::json parsed;
1205 try {
1206 parsed = nlohmann::json::parse(it->second);
1207 } catch (const nlohmann::json::parse_error& e) {
1209 absl::StrCat("Apply failed: invalid JSON (", e.what(), ").");
1211 return;
1212 }
1213
1214 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1215 if (!tree_or.ok()) {
1217 absl::StrCat("Apply failed: ", tree_or.status().message());
1219 return;
1220 }
1221
1222 std::string validation_error;
1223 if (!tree_or->Validate(&validation_error)) {
1225 absl::StrCat("Apply failed: invalid layout (", validation_error, ").");
1227 return;
1228 }
1229
1231 if (manager == nullptr) {
1232 workspace_status_message_ = "Apply failed: LayoutManager unavailable.";
1234 return;
1235 }
1236 const ImGuiID dockspace_id = manager->GetMainDockspaceId();
1237 if (dockspace_id == 0) {
1238 workspace_status_message_ = "Apply failed: main dockspace not yet created.";
1240 return;
1241 }
1242
1243 const absl::Status apply_status =
1244 manager->ApplyDockTree(*tree_or, dockspace_id);
1245 if (!apply_status.ok()) {
1247 absl::StrCat("Apply failed: ", apply_status.message());
1249 return;
1250 }
1251
1252 prefs.last_applied_layout_name = name;
1253 (void)user_settings_->Save();
1254 workspace_status_message_ = absl::StrCat("Applied \"", name, "\".");
1256}
1257
1259 if (!user_settings_)
1260 return;
1261
1262 ImGui::Text(tr("%s Auto-Save"), ICON_MD_SAVE);
1263 ImGui::Separator();
1264
1265 if (ImGui::Checkbox(tr("Enable Auto-Save"),
1268 }
1269
1271 ImGui::Indent();
1272 int interval = static_cast<int>(user_settings_->prefs().autosave_interval);
1273 if (ImGui::SliderInt(tr("Interval (sec)"), &interval, 60, 600)) {
1274 user_settings_->prefs().autosave_interval = static_cast<float>(interval);
1276 }
1277
1278 if (ImGui::Checkbox(tr("Backup Before Save"),
1281 }
1282 ImGui::Unindent();
1283 }
1284
1285 ImGui::Spacing();
1286 ImGui::Text(tr("%s Recent Files"), ICON_MD_HISTORY);
1287 ImGui::Separator();
1288
1289 if (ImGui::SliderInt(tr("Limit"), &user_settings_->prefs().recent_files_limit,
1290 5, 50)) {
1292 }
1293
1294 ImGui::Spacing();
1295 ImGui::Text(tr("%s Default Editor"), ICON_MD_EDIT);
1296 ImGui::Separator();
1297
1298 const char* editors[] = {"None", "Overworld", "Dungeon", "Graphics"};
1299 if (ImGui::Combo("##DefaultEditor", &user_settings_->prefs().default_editor,
1300 editors, IM_ARRAYSIZE(editors))) {
1302 }
1303
1304 ImGui::Spacing();
1305 ImGui::Text(tr("%s Sprite Names"), ICON_MD_LABEL);
1306 ImGui::Separator();
1307 if (ImGui::Checkbox(tr("Use HMagic sprite names (expanded)"),
1312 }
1313
1314 ImGui::Spacing();
1315 ImGui::Text(tr("%s Testing"), ICON_MD_SCIENCE);
1316 ImGui::Separator();
1317 if (ImGui::Checkbox(tr("Test mode"), &user_settings_->prefs().test_mode)) {
1319 }
1320 if (ImGui::IsItemHovered()) {
1321 ImGui::SetTooltip(
1322 tr("From the next launch: no welcome screen, no editor picker, and "
1323 "the local test server starts on port 50052 (builds with gRPC)."));
1324 }
1325
1326 ImGui::Spacing();
1327 ImGui::Text(tr("%s Onboarding Tips"), ICON_MD_LIGHTBULB);
1328 ImGui::Separator();
1329 if (ImGui::Checkbox(tr("Show feature tips (ZScream migration, etc.)"),
1332 }
1333 ImGui::TextDisabled(tr("%zu tip(s) already dismissed"),
1335 if (ImGui::Button(tr("Reset dismissed tips"))) {
1338 }
1339}
1340
1342 if (!user_settings_)
1343 return;
1344
1345 ImGui::Text(tr("%s Graphics"), ICON_MD_IMAGE);
1346 ImGui::Separator();
1347
1348 if (ImGui::Checkbox(tr("V-Sync"), &user_settings_->prefs().vsync)) {
1350 }
1351
1352 if (ImGui::SliderInt(tr("Target FPS"), &user_settings_->prefs().target_fps,
1353 30, 144)) {
1355 }
1356
1357 ImGui::Spacing();
1358 ImGui::Text(tr("%s Memory"), ICON_MD_MEMORY);
1359 ImGui::Separator();
1360
1361 if (ImGui::SliderInt(tr("Cache Size (MB)"),
1362 &user_settings_->prefs().cache_size_mb, 128, 2048)) {
1364 }
1365
1366 if (ImGui::SliderInt(tr("Undo History"),
1367 &user_settings_->prefs().undo_history_size, 10, 200)) {
1369 }
1370
1371 ImGui::Spacing();
1372 ImGui::Separator();
1373 ImGui::Text(tr("Current FPS: %.1f"), ImGui::GetIO().Framerate);
1374 ImGui::Text(tr("Frame Time: %.3f ms"), 1000.0f / ImGui::GetIO().Framerate);
1375}
1376
1378 if (!user_settings_)
1379 return;
1380
1381 auto& prefs = user_settings_->prefs();
1382 auto& hosts = prefs.ai_hosts;
1383 static int selected_host_index = -1;
1384
1385 auto draw_key_row = [&](const char* label, std::string* key,
1386 const char* env_var, const char* id) {
1387 ImGui::PushID(id);
1388 ImGui::Text("%s", label);
1389 const ImVec2 button_size = ImGui::CalcTextSize(ICON_MD_SYNC " Env");
1390 float env_button_width =
1391 button_size.x + ImGui::GetStyle().FramePadding.x * 2.0f;
1392 float input_width = ImGui::GetContentRegionAvail().x - env_button_width -
1393 ImGui::GetStyle().ItemSpacing.x;
1394 bool stack = input_width < 160.0f;
1395 ImGui::SetNextItemWidth(stack ? -1.0f : input_width);
1396 if (ImGui::InputTextWithHint("##key", "API key...", key,
1397 ImGuiInputTextFlags_Password)) {
1399 }
1400 if (!stack) {
1401 ImGui::SameLine();
1402 }
1403 if (ImGui::SmallButton(ICON_MD_SYNC " Env")) {
1404 const char* env_key = std::getenv(env_var);
1405 if (env_key) {
1406 *key = env_key;
1408 }
1409 }
1410 ImGui::Spacing();
1411 ImGui::PopID();
1412 };
1413
1414 ImGui::Text(tr("%s Provider Keys"), ICON_MD_VPN_KEY);
1415 ImGui::Separator();
1416 draw_key_row("OpenAI", &prefs.openai_api_key, "OPENAI_API_KEY", "openai_key");
1417 draw_key_row("Anthropic", &prefs.anthropic_api_key, "ANTHROPIC_API_KEY",
1418 "anthropic_key");
1419 draw_key_row("Google (Gemini)", &prefs.gemini_api_key, "GEMINI_API_KEY",
1420 "gemini_key");
1421 ImGui::Spacing();
1422
1423 // Provider selection
1424 ImGui::Text(tr("%s Provider Defaults (legacy)"), ICON_MD_CLOUD);
1425 ImGui::Separator();
1426
1427 const char* providers[] = {"Ollama (Local)", "Gemini (Cloud)",
1428 "Mock (Testing)"};
1429 if (ImGui::Combo("##Provider", &prefs.ai_provider, providers,
1430 IM_ARRAYSIZE(providers))) {
1432 }
1433
1434 ImGui::Spacing();
1435 ImGui::Text(tr("%s Host Routing"), ICON_MD_STORAGE);
1436 ImGui::Separator();
1437
1438 const char* active_preview = "None";
1439 const char* remote_preview = "None";
1440 for (const auto& host : hosts) {
1441 if (!prefs.active_ai_host_id.empty() &&
1442 host.id == prefs.active_ai_host_id) {
1443 active_preview = host.label.c_str();
1444 }
1445 if (!prefs.remote_build_host_id.empty() &&
1446 host.id == prefs.remote_build_host_id) {
1447 remote_preview = host.label.c_str();
1448 }
1449 }
1450
1451 if (ImGui::BeginCombo(tr("Active Host"), active_preview)) {
1452 for (size_t i = 0; i < hosts.size(); ++i) {
1453 const bool is_selected = (!prefs.active_ai_host_id.empty() &&
1454 hosts[i].id == prefs.active_ai_host_id);
1455 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1456 prefs.active_ai_host_id = hosts[i].id;
1457 if (prefs.remote_build_host_id.empty()) {
1458 prefs.remote_build_host_id = hosts[i].id;
1459 }
1461 }
1462 if (is_selected) {
1463 ImGui::SetItemDefaultFocus();
1464 }
1465 }
1466 ImGui::EndCombo();
1467 }
1468
1469 if (ImGui::BeginCombo(tr("Remote Build Host"), remote_preview)) {
1470 for (size_t i = 0; i < hosts.size(); ++i) {
1471 const bool is_selected = (!prefs.remote_build_host_id.empty() &&
1472 hosts[i].id == prefs.remote_build_host_id);
1473 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1474 prefs.remote_build_host_id = hosts[i].id;
1476 }
1477 if (is_selected) {
1478 ImGui::SetItemDefaultFocus();
1479 }
1480 }
1481 ImGui::EndCombo();
1482 }
1483
1484 ImGui::Spacing();
1485 ImGui::Text(tr("%s AI Hosts"), ICON_MD_STORAGE);
1486 ImGui::Separator();
1487
1488 if (selected_host_index >= static_cast<int>(hosts.size())) {
1489 selected_host_index = hosts.empty() ? -1 : 0;
1490 }
1491 if (selected_host_index < 0 && !hosts.empty()) {
1492 for (size_t i = 0; i < hosts.size(); ++i) {
1493 if (!prefs.active_ai_host_id.empty() &&
1494 hosts[i].id == prefs.active_ai_host_id) {
1495 selected_host_index = static_cast<int>(i);
1496 break;
1497 }
1498 }
1499 if (selected_host_index < 0) {
1500 selected_host_index = 0;
1501 }
1502 }
1503
1504 const float hosts_height = std::clamp(
1505 ImGui::GetContentRegionAvail().y * 0.32f, gui::ScaledSize(0.0f, 120.0f).y,
1506 gui::ScaledSize(0.0f, 240.0f).y);
1507 ImGui::BeginChild("##ai_host_list", ImVec2(0, hosts_height), true);
1508 for (size_t i = 0; i < hosts.size(); ++i) {
1509 const bool is_selected = static_cast<int>(i) == selected_host_index;
1510 std::string label = hosts[i].label;
1511 if (hosts[i].id == prefs.active_ai_host_id) {
1512 label += " (active)";
1513 }
1514 if (hosts[i].id == prefs.remote_build_host_id) {
1515 label += " (build)";
1516 }
1517 if (ImGui::Selectable(label.c_str(), is_selected)) {
1518 selected_host_index = static_cast<int>(i);
1519 }
1520 std::string tags = BuildHostTagString(hosts[i]);
1521 if (!tags.empty()) {
1522 ImGui::SameLine();
1523 ImGui::TextDisabled("%s", tags.c_str());
1524 }
1525 }
1526 ImGui::EndChild();
1527
1528 auto add_host = [&](UserSettings::Preferences::AiHost host) {
1529 if (host.id.empty()) {
1530 host.id = absl::StrFormat("host-%zu", hosts.size() + 1);
1531 }
1532 hosts.push_back(host);
1533 selected_host_index = static_cast<int>(hosts.size() - 1);
1534 if (prefs.active_ai_host_id.empty()) {
1535 prefs.active_ai_host_id = host.id;
1536 }
1537 if (prefs.remote_build_host_id.empty()) {
1538 prefs.remote_build_host_id = host.id;
1539 }
1541 };
1542
1543 if (ImGui::Button(ICON_MD_ADD " Add Host")) {
1545 host.label = "New Host";
1546 host.base_url = "http://localhost:1234";
1547 host.api_type = "openai";
1548 add_host(host);
1549 }
1550 ImGui::SameLine();
1551 if (ImGui::Button(ICON_MD_DELETE " Remove") && selected_host_index >= 0 &&
1552 selected_host_index < static_cast<int>(hosts.size())) {
1553 const std::string removed_id = hosts[selected_host_index].id;
1554 hosts.erase(hosts.begin() + selected_host_index);
1555 if (prefs.active_ai_host_id == removed_id) {
1556 prefs.active_ai_host_id = hosts.empty() ? "" : hosts.front().id;
1557 }
1558 if (prefs.remote_build_host_id == removed_id) {
1559 prefs.remote_build_host_id = prefs.active_ai_host_id;
1560 }
1561 selected_host_index =
1562 hosts.empty()
1563 ? -1
1564 : std::min(selected_host_index, static_cast<int>(hosts.size() - 1));
1566 }
1567
1568 ImGui::SameLine();
1569 if (ImGui::Button(tr("Add LM Studio"))) {
1571 host.label = "LM Studio (local)";
1572 host.base_url = "http://localhost:1234";
1574 host.supports_tools = true;
1575 host.supports_streaming = true;
1576 add_host(host);
1577 }
1578 ImGui::SameLine();
1579 if (ImGui::Button(tr("Add AFS Bridge"))) {
1581 host.label = "halext AFS Bridge";
1582 host.base_url = "https://halext.org";
1584 host.supports_tools = true;
1585 host.supports_streaming = true;
1586 add_host(host);
1587 }
1588 ImGui::SameLine();
1589 if (ImGui::Button(tr("Add Ollama"))) {
1591 host.label = "Ollama (local)";
1592 host.base_url = "http://localhost:11434";
1594 host.supports_tools = true;
1595 host.supports_streaming = true;
1596 add_host(host);
1597 }
1598
1599 static std::string tailscale_host;
1600 ImGui::InputTextWithHint("##tailscale_host", "host.ts.net:1234",
1601 &tailscale_host);
1602 ImGui::SameLine();
1603 if (ImGui::Button(tr("Add Tailscale Host"))) {
1604 std::string trimmed =
1605 std::string(absl::StripAsciiWhitespace(tailscale_host));
1606 if (!trimmed.empty()) {
1608 host.label = "Tailscale Host";
1609 if (absl::StrContains(trimmed, "://")) {
1610 host.base_url = trimmed;
1611 } else {
1612 host.base_url = "http://" + trimmed;
1613 }
1615 host.supports_tools = true;
1616 host.supports_streaming = true;
1617 host.allow_insecure = true;
1618 add_host(host);
1619 tailscale_host.clear();
1620 }
1621 }
1622
1623 if (selected_host_index >= 0 &&
1624 selected_host_index < static_cast<int>(hosts.size())) {
1625 auto& host = hosts[static_cast<size_t>(selected_host_index)];
1626 ImGui::Spacing();
1627 ImGui::Text(tr("Host Details"));
1628 ImGui::Separator();
1629 if (ImGui::InputText(tr("Label"), &host.label)) {
1631 }
1632 if (ImGui::InputText(tr("Base URL"), &host.base_url)) {
1634 }
1635
1636 const char* api_types[] = {"openai", "ollama", "gemini",
1637 "anthropic", "lmstudio", "grpc"};
1638 int api_index = 0;
1639 for (int i = 0; i < IM_ARRAYSIZE(api_types); ++i) {
1640 if (host.api_type == api_types[i]) {
1641 api_index = i;
1642 break;
1643 }
1644 }
1645 if (ImGui::Combo(tr("API Type"), &api_index, api_types,
1646 IM_ARRAYSIZE(api_types))) {
1647 host.api_type = api_types[api_index];
1649 }
1650
1651 if (ImGui::InputText(tr("API Key"), &host.api_key,
1652 ImGuiInputTextFlags_Password)) {
1654 }
1655 if (ImGui::InputText(tr("Keychain ID"), &host.credential_id)) {
1657 }
1658 ImGui::SameLine();
1659 if (ImGui::SmallButton(tr("Use Host ID"))) {
1660 host.credential_id = host.id;
1662 }
1663 if (!host.credential_id.empty() && host.api_key.empty()) {
1664 ImGui::TextDisabled(tr("Keychain lookup enabled (leave API key empty)."));
1665 }
1666
1667 if (ImGui::Checkbox(tr("Supports Vision"), &host.supports_vision)) {
1669 }
1670 ImGui::SameLine();
1671 if (ImGui::Checkbox(tr("Supports Tools"), &host.supports_tools)) {
1673 }
1674 ImGui::SameLine();
1675 if (ImGui::Checkbox(tr("Supports Streaming"), &host.supports_streaming)) {
1677 }
1678 if (ImGui::Checkbox(tr("Allow Insecure HTTP"), &host.allow_insecure)) {
1680 }
1681 }
1682
1683 ImGui::Spacing();
1684 ImGui::Text(tr("%s Local Model Paths"), ICON_MD_FOLDER);
1685 ImGui::Separator();
1686
1687 auto& model_paths = prefs.ai_model_paths;
1688 static int selected_model_path = -1;
1689 static std::string new_model_path;
1690
1691 if (model_paths.empty()) {
1692 ImGui::TextDisabled(tr("No model paths configured."));
1693 }
1694
1695 const float model_paths_height = std::clamp(
1696 ImGui::GetContentRegionAvail().y * 0.24f, gui::ScaledSize(0.0f, 96.0f).y,
1697 gui::ScaledSize(0.0f, 180.0f).y);
1698 if (ImGui::BeginChild("ModelPathsList", ImVec2(0, model_paths_height),
1699 true)) {
1700 for (size_t i = 0; i < model_paths.size(); ++i) {
1701 std::string label =
1703 if (ImGui::Selectable(label.c_str(),
1704 selected_model_path == static_cast<int>(i))) {
1705 selected_model_path = static_cast<int>(i);
1706 }
1707 }
1708 }
1709 ImGui::EndChild();
1710
1711 const bool has_model_selection =
1712 selected_model_path >= 0 &&
1713 selected_model_path < static_cast<int>(model_paths.size());
1714 if (has_model_selection) {
1715 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
1716 model_paths.erase(model_paths.begin() + selected_model_path);
1717 selected_model_path =
1718 model_paths.empty()
1719 ? -1
1720 : std::min(selected_model_path,
1721 static_cast<int>(model_paths.size() - 1));
1723 }
1724 }
1725
1726 ImGui::Spacing();
1727 ImGui::InputTextWithHint("##model_path_add", "Add folder path...",
1728 &new_model_path);
1729 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
1730 const std::string trimmed =
1731 std::string(absl::StripAsciiWhitespace(new_model_path));
1732 if (!trimmed.empty() && AddUniquePath(&model_paths, trimmed)) {
1734 new_model_path.clear();
1735 }
1736 }
1737 ImGui::SameLine();
1738 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
1739 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
1740 if (!folder.empty() && AddUniquePath(&model_paths, folder)) {
1742 }
1743 }
1744
1745 ImGui::Spacing();
1746 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
1747 ImGui::Separator();
1748 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
1749 if (ImGui::Button(ICON_MD_HOME " Add ~/models")) {
1750 if (!home_dir.empty() && home_dir != ".") {
1751 if (AddUniquePath(&model_paths, (home_dir / "models").string())) {
1753 }
1754 }
1755 }
1756 ImGui::SameLine();
1757 if (ImGui::Button(tr("Add ~/.lmstudio/models"))) {
1758 if (!home_dir.empty() && home_dir != ".") {
1759 if (AddUniquePath(&model_paths,
1760 (home_dir / ".lmstudio" / "models").string())) {
1762 }
1763 }
1764 }
1765 ImGui::SameLine();
1766 if (ImGui::Button(tr("Add ~/.ollama/models"))) {
1767 if (!home_dir.empty() && home_dir != ".") {
1768 if (AddUniquePath(&model_paths,
1769 (home_dir / ".ollama" / "models").string())) {
1771 }
1772 }
1773 }
1774
1775 ImGui::Spacing();
1776 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
1777 ImGui::Separator();
1778
1779 if (ImGui::SliderFloat(tr("Temperature"),
1780 &user_settings_->prefs().ai_temperature, 0.0f, 2.0f)) {
1782 }
1783 ImGui::TextDisabled(tr("Higher = more creative"));
1784
1785 if (ImGui::SliderInt(tr("Max Tokens"), &user_settings_->prefs().ai_max_tokens,
1786 256, 8192)) {
1788 }
1789
1790 ImGui::Spacing();
1791 ImGui::Text(tr("%s Behavior"), ICON_MD_PSYCHOLOGY);
1792 ImGui::Separator();
1793
1794 if (ImGui::Checkbox(tr("Proactive Suggestions"),
1797 }
1798
1799 if (ImGui::Checkbox(tr("Auto-Learn Preferences"),
1802 }
1803
1804 if (ImGui::Checkbox(tr("Enable Vision"),
1807 }
1808
1809 ImGui::Spacing();
1810 ImGui::Text(tr("%s Logging"), ICON_MD_TERMINAL);
1811 ImGui::Separator();
1812
1813 const char* log_levels[] = {"Debug", "Info", "Warning", "Error", "Fatal"};
1814 if (ImGui::Combo(tr("Log Level"), &user_settings_->prefs().log_level,
1815 log_levels, IM_ARRAYSIZE(log_levels))) {
1816 // Apply log level logic here if needed
1818 }
1819}
1820
1822 if (ImGui::TreeNodeEx(ICON_MD_KEYBOARD " Shortcuts",
1823 ImGuiTreeNodeFlags_DefaultOpen)) {
1824 ImGui::InputTextWithHint("##shortcut_filter", "Filter shortcuts...",
1826 if (ImGui::IsItemHovered()) {
1827 ImGui::SetTooltip(tr("Filter by action name or key combo"));
1828 }
1829 ImGui::Spacing();
1830
1831 if (ImGui::TreeNode("Global Shortcuts")) {
1833 ImGui::TreePop();
1834 }
1835 if (ImGui::TreeNode("Editor Shortcuts")) {
1837 ImGui::TreePop();
1838 }
1839 if (ImGui::TreeNode("Panel Shortcuts")) {
1841 ImGui::TreePop();
1842 }
1843 ImGui::TextDisabled(
1844 tr("Tip: Use Cmd/Opt labels on macOS or Ctrl/Alt on Windows/Linux. "
1845 "Function keys and symbols (/, -) are supported."));
1846 ImGui::TreePop();
1847 }
1848}
1849
1850bool SettingsPanel::MatchesShortcutFilter(const std::string& text) const {
1851 if (shortcut_filter_.empty()) {
1852 return true;
1853 }
1854 std::string haystack = absl::AsciiStrToLower(text);
1855 std::string needle = absl::AsciiStrToLower(shortcut_filter_);
1856 return absl::StrContains(haystack, needle);
1857}
1858
1859namespace {
1860
1861// One editable binding row: text field (Enter applies), conflict warning, and
1862// a reset button when the binding differs from its registered default.
1863// `overrides` is the persisted map for this scope (global_shortcuts or
1864// editor_shortcuts): absent = default, "" = explicitly unbound.
1866 const Shortcut& sc, std::string& value, ShortcutManager* shortcut_manager,
1867 UserSettings* user_settings,
1868 std::unordered_map<std::string, std::string>& overrides) {
1869 ImGui::PushID(sc.name.c_str());
1870 ImGui::Text("%s", sc.name.c_str());
1871 ImGui::SameLine();
1872 ImGui::SetNextItemWidth(std::clamp(ImGui::GetContentRegionAvail().x * 0.45f,
1873 gui::ScaledSize(160.0f, 0.0f).x,
1874 gui::ScaledSize(300.0f, 0.0f).x));
1875 if (ImGui::InputText("##binding", &value,
1876 ImGuiInputTextFlags_EnterReturnsTrue |
1877 ImGuiInputTextFlags_AutoSelectAll)) {
1878 auto parsed = ParseShortcut(value);
1879 if (!parsed.empty() || value.empty()) {
1880 // Empty string unbinds the shortcut (persisted as "").
1881 shortcut_manager->UpdateShortcutKeys(sc.name, parsed);
1882 overrides[sc.name] = value;
1883 value = PrintShortcut(parsed);
1884 user_settings->Save();
1885 }
1886 }
1887
1888 const Shortcut* live = shortcut_manager->FindShortcut(sc.name);
1889 if (live) {
1890 const auto conflicts = shortcut_manager->FindConflicts(sc.name);
1891 if (!conflicts.empty()) {
1892 ImGui::SameLine();
1893 ImGui::TextColored(gui::GetWarningColor(), ICON_MD_WARNING);
1894 if (ImGui::IsItemHovered()) {
1895 std::string tip = "Also bound to:";
1896 for (const auto& other : conflicts) {
1897 tip += "\n " + other;
1898 }
1899 ImGui::SetTooltip("%s", tip.c_str());
1900 }
1901 }
1902 if (live->keys != live->default_keys) {
1903 ImGui::SameLine();
1904 if (ImGui::SmallButton(tr("Reset"))) {
1905 shortcut_manager->ResetShortcutKeys(sc.name);
1906 overrides.erase(sc.name);
1907 value = PrintShortcut(live->default_keys);
1908 user_settings->Save();
1909 }
1910 if (ImGui::IsItemHovered()) {
1911 ImGui::SetTooltip("Default: %s",
1912 live->default_keys.empty()
1913 ? "(none)"
1914 : PrintShortcut(live->default_keys).c_str());
1915 }
1916 }
1917 }
1918 ImGui::PopID();
1919}
1920
1921} // namespace
1922
1925 ImGui::TextDisabled(tr("Not available"));
1926 return;
1927 }
1928
1929 auto shortcuts =
1931 if (shortcuts.empty()) {
1932 ImGui::TextDisabled(tr("No global shortcuts registered."));
1933 return;
1934 }
1935 std::sort(
1936 shortcuts.begin(), shortcuts.end(),
1937 [](const Shortcut& a, const Shortcut& b) { return a.name < b.name; });
1938
1939 static std::unordered_map<std::string, std::string> editing;
1940
1941 bool has_match = false;
1942 for (const auto& sc : shortcuts) {
1943 if (sc.keys.empty() && sc.default_keys.empty()) {
1944 continue; // Command-palette-only entry; nothing to bind here.
1945 }
1946 std::string keys = PrintShortcut(sc.keys);
1947 if (!MatchesShortcutFilter(sc.name) && !MatchesShortcutFilter(keys)) {
1948 continue;
1949 }
1950 has_match = true;
1951 auto it = editing.find(sc.name);
1952 if (it == editing.end()) {
1953 it = editing.emplace(sc.name, keys).first;
1954 }
1955 DrawShortcutBindingRow(sc, it->second, shortcut_manager_, user_settings_,
1957 }
1958 if (!has_match) {
1959 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
1960 }
1961}
1962
1965 ImGui::TextDisabled(tr("Not available"));
1966 return;
1967 }
1968
1969 auto shortcuts =
1971 std::map<std::string, std::vector<Shortcut>> grouped;
1972 static std::unordered_map<std::string, std::string> editing;
1973
1974 for (const auto& sc : shortcuts) {
1975 if (sc.keys.empty() && sc.default_keys.empty()) {
1976 continue; // Command-palette-only entry; nothing to bind here.
1977 }
1978 auto pos = sc.name.find(".");
1979 std::string group =
1980 pos != std::string::npos ? sc.name.substr(0, pos) : "general";
1981 grouped[group].push_back(sc);
1982 }
1983 bool has_match = false;
1984 for (auto& [group, list] : grouped) {
1985 std::sort(
1986 list.begin(), list.end(),
1987 [](const Shortcut& a, const Shortcut& b) { return a.name < b.name; });
1988 std::vector<Shortcut> filtered;
1989 filtered.reserve(list.size());
1990 for (const auto& sc : list) {
1991 std::string keys = PrintShortcut(sc.keys);
1992 if (MatchesShortcutFilter(sc.name) || MatchesShortcutFilter(keys)) {
1993 filtered.push_back(sc);
1994 }
1995 }
1996 if (filtered.empty()) {
1997 continue;
1998 }
1999 has_match = true;
2000 if (ImGui::TreeNode(group.c_str())) {
2001 for (const auto& sc : filtered) {
2002 auto it = editing.find(sc.name);
2003 if (it == editing.end()) {
2004 it = editing.emplace(sc.name, PrintShortcut(sc.keys)).first;
2005 }
2006 DrawShortcutBindingRow(sc, it->second, shortcut_manager_,
2009 }
2010 ImGui::TreePop();
2011 }
2012 }
2013 if (!has_match) {
2014 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
2015 }
2016}
2017
2020 ImGui::TextDisabled(tr("Registry not available"));
2021 return;
2022 }
2023
2024 // Simplified shortcut editor for sidebar
2025 auto categories = window_manager_->GetAllCategories();
2026
2027 bool has_match = false;
2028 for (const auto& category : categories) {
2029 auto cards = window_manager_->GetWindowsInCategory(0, category);
2030 std::vector<decltype(cards)::value_type> filtered_cards;
2031 filtered_cards.reserve(cards.size());
2032 for (const auto& card : cards) {
2033 if (MatchesShortcutFilter(card.display_name) ||
2034 MatchesShortcutFilter(card.card_id)) {
2035 filtered_cards.push_back(card);
2036 }
2037 }
2038 if (filtered_cards.empty()) {
2039 continue;
2040 }
2041 has_match = true;
2042 if (ImGui::TreeNode(category.c_str())) {
2043
2044 for (const auto& card : filtered_cards) {
2045 ImGui::PushID(card.card_id.c_str());
2046
2047 ImGui::Text("%s %s", card.icon.c_str(), card.display_name.c_str());
2048
2049 std::string current_shortcut;
2050 auto it = user_settings_->prefs().panel_shortcuts.find(card.card_id);
2051 if (it != user_settings_->prefs().panel_shortcuts.end()) {
2052 current_shortcut = it->second;
2053 } else if (!card.shortcut_hint.empty()) {
2054 current_shortcut = card.shortcut_hint;
2055 } else {
2056 current_shortcut = "None";
2057 }
2058
2059 // Display platform-aware label
2060 std::string display_shortcut = current_shortcut;
2061 auto parsed = ParseShortcut(current_shortcut);
2062 if (!parsed.empty()) {
2063 display_shortcut = PrintShortcut(parsed);
2064 }
2065
2066 if (is_editing_shortcut_ && editing_card_id_ == card.card_id) {
2067 ImGui::SetNextItemWidth(
2068 std::clamp(ImGui::GetContentRegionAvail().x * 0.40f,
2069 gui::ScaledSize(100.0f, 0.0f).x,
2070 gui::ScaledSize(220.0f, 0.0f).x));
2071 ImGui::SetKeyboardFocusHere();
2072 if (ImGui::InputText("##Edit", shortcut_edit_buffer_,
2073 sizeof(shortcut_edit_buffer_),
2074 ImGuiInputTextFlags_EnterReturnsTrue)) {
2075 if (strlen(shortcut_edit_buffer_) > 0) {
2076 user_settings_->prefs().panel_shortcuts[card.card_id] =
2078 } else {
2079 user_settings_->prefs().panel_shortcuts.erase(card.card_id);
2080 }
2082 is_editing_shortcut_ = false;
2083 editing_card_id_.clear();
2084 }
2085 ImGui::SameLine();
2086 if (ImGui::Button(ICON_MD_CLOSE)) {
2087 is_editing_shortcut_ = false;
2088 editing_card_id_.clear();
2089 }
2090 } else {
2091 if (ImGui::Button(display_shortcut.c_str(), ImVec2(120, 0))) {
2092 is_editing_shortcut_ = true;
2093 editing_card_id_ = card.card_id;
2094 strncpy(shortcut_edit_buffer_, current_shortcut.c_str(),
2095 sizeof(shortcut_edit_buffer_) - 1);
2096 }
2097 if (ImGui::IsItemHovered()) {
2098 ImGui::SetTooltip(tr("Click to edit shortcut"));
2099 }
2100 }
2101
2102 ImGui::PopID();
2103 }
2104
2105 ImGui::TreePop();
2106 }
2107 }
2108 if (!has_match) {
2109 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
2110 }
2111}
2112
2114 // Load patches on first access
2115 if (!patches_loaded_) {
2116 // Try to load from default patches location
2117 auto patches_dir_status = util::PlatformPaths::FindAsset("patches");
2118 if (patches_dir_status.ok()) {
2119 auto status = patch_manager_.LoadPatches(patches_dir_status->string());
2120 if (status.ok()) {
2121 patches_loaded_ = true;
2122 if (!patch_manager_.folders().empty()) {
2124 }
2125 }
2126 }
2127 }
2128
2129 ImGui::Text(tr("%s ZScream Patch System"), ICON_MD_EXTENSION);
2130 ImGui::Separator();
2131
2132 if (!patches_loaded_) {
2133 ImGui::TextDisabled(tr("No patches loaded"));
2134 ImGui::TextDisabled(tr("Place .asm patches in assets/patches/"));
2135
2136 if (ImGui::Button(tr("Browse for Patches Folder..."))) {
2137 // TODO: File browser for patches folder
2138 }
2139 return;
2140 }
2141
2142 // Status line
2143 int enabled_count = patch_manager_.GetEnabledPatchCount();
2144 int total_count = static_cast<int>(patch_manager_.patches().size());
2145 ImGui::Text(tr("Loaded: %d patches (%d enabled)"), total_count,
2146 enabled_count);
2147
2148 ImGui::Spacing();
2149
2150 // Folder tabs
2151 if (gui::BeginThemedTabBar("##PatchFolders",
2152 ImGuiTabBarFlags_FittingPolicyScroll)) {
2153 for (const auto& folder : patch_manager_.folders()) {
2154 if (ImGui::BeginTabItem(folder.c_str())) {
2155 selected_folder_ = folder;
2156 DrawPatchList(folder);
2157 ImGui::EndTabItem();
2158 }
2159 }
2161 }
2162
2163 ImGui::Spacing();
2164 ImGui::Separator();
2165
2166 // Selected patch details
2167 if (selected_patch_) {
2169 } else {
2170 ImGui::TextDisabled(tr("Select a patch to view details"));
2171 }
2172
2173 ImGui::Spacing();
2174 ImGui::Separator();
2175
2176 // Action buttons
2177 if (ImGui::Button(ICON_MD_CHECK " Apply Patches to ROM")) {
2178 if (rom_ && rom_->is_loaded()) {
2179#ifdef YAZE_WITH_Z3DK
2180 if (project_) {
2182 const auto& z3dk = project_->z3dk_settings;
2183 options.include_paths = z3dk.include_paths;
2184 options.defines = z3dk.defines;
2185 options.std_includes_path = z3dk.std_includes_path;
2186 options.std_defines_path = z3dk.std_defines_path;
2187 options.mapper = z3dk.mapper;
2188 options.rom_size = z3dk.rom_size;
2189 options.capture_nocash_symbols = (z3dk.symbols_format == "nocash");
2190 options.warn_unused_symbols = z3dk.warn_unused_symbols;
2191 options.warn_branch_outside_bank = z3dk.warn_branch_outside_bank;
2192 options.warn_unknown_width = z3dk.warn_unknown_width;
2193 options.warn_org_collision = z3dk.warn_org_collision;
2194 options.warn_unauthorized_hook = z3dk.warn_unauthorized_hook;
2195 options.warn_stack_balance = z3dk.warn_stack_balance;
2196 options.warn_hook_return = z3dk.warn_hook_return;
2197 for (const auto& range : z3dk.prohibited_memory_ranges) {
2198 options.prohibited_memory_ranges.push_back(
2199 {.start = range.start, .end = range.end, .reason = range.reason});
2200 }
2201 if (!project_->code_folder.empty() &&
2202 std::find(options.include_paths.begin(),
2203 options.include_paths.end(),
2204 project_->code_folder) == options.include_paths.end()) {
2205 options.include_paths.push_back(project_->code_folder);
2206 }
2207 if (!z3dk.rom_path.empty()) {
2208 options.hooks_rom_path = z3dk.rom_path;
2209 }
2210 if (!rom_->filename().empty()) {
2211 options.hooks_rom_path = rom_->filename();
2212 }
2213 patch_manager_.SetZ3dkAssembleOptions(options);
2214 }
2215#endif
2217 if (!status.ok()) {
2218 LOG_ERROR("Settings", "Failed to apply patches: %s", status.message());
2219 } else {
2220 LOG_INFO("Settings", "Applied %d patches successfully", enabled_count);
2221 }
2222 } else {
2223 LOG_WARN("Settings", "No ROM loaded");
2224 }
2225 }
2226 if (ImGui::IsItemHovered()) {
2227 ImGui::SetTooltip(tr("Apply all enabled patches to the loaded ROM"));
2228 }
2229
2230 ImGui::SameLine();
2231 if (ImGui::Button(ICON_MD_SAVE " Save All")) {
2232 auto status = patch_manager_.SaveAllPatches();
2233 if (!status.ok()) {
2234 LOG_ERROR("Settings", "Failed to save patches: %s", status.message());
2235 }
2236 }
2237
2238 if (ImGui::Button(ICON_MD_REFRESH " Reload Patches")) {
2239 patches_loaded_ = false;
2240 selected_patch_ = nullptr;
2241 }
2242}
2243
2244void SettingsPanel::DrawPatchList(const std::string& folder) {
2245 auto patches = patch_manager_.GetPatchesInFolder(folder);
2246
2247 if (patches.empty()) {
2248 ImGui::TextDisabled(tr("No patches in this folder"));
2249 return;
2250 }
2251
2252 // Use a child region for scrolling
2253 float available_height = std::min(200.0f, patches.size() * 25.0f + 10.0f);
2254 if (ImGui::BeginChild("##PatchList", ImVec2(0, available_height), true)) {
2255 for (auto* patch : patches) {
2256 ImGui::PushID(patch->filename().c_str());
2257
2258 bool enabled = patch->enabled();
2259 if (ImGui::Checkbox("##Enabled", &enabled)) {
2260 patch->set_enabled(enabled);
2261 }
2262
2263 ImGui::SameLine();
2264
2265 // Highlight selected patch
2266 bool is_selected = (selected_patch_ == patch);
2267 if (ImGui::Selectable(patch->name().c_str(), is_selected)) {
2268 selected_patch_ = patch;
2269 }
2270
2271 ImGui::PopID();
2272 }
2273 }
2274 ImGui::EndChild();
2275}
2276
2278 if (!selected_patch_)
2279 return;
2280
2281 ImGui::Text("%s %s", ICON_MD_INFO, selected_patch_->name().c_str());
2282
2283 if (!selected_patch_->author().empty()) {
2284 ImGui::TextDisabled(tr("by %s"), selected_patch_->author().c_str());
2285 }
2286
2287 if (!selected_patch_->version().empty()) {
2288 ImGui::SameLine();
2289 ImGui::TextDisabled(tr("v%s"), selected_patch_->version().c_str());
2290 }
2291
2292 // Description
2293 if (!selected_patch_->description().empty()) {
2294 ImGui::Spacing();
2295 ImGui::TextWrapped("%s", selected_patch_->description().c_str());
2296 }
2297
2298 // Parameters
2299 auto& params = selected_patch_->mutable_parameters();
2300 if (!params.empty()) {
2301 ImGui::Spacing();
2302 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
2303 ImGui::Separator();
2304
2305 for (auto& param : params) {
2306 DrawParameterWidget(&param);
2307 }
2308 }
2309}
2310
2312 if (!param)
2313 return;
2314
2315 ImGui::PushID(param->define_name.c_str());
2316
2317 switch (param->type) {
2321 int value = param->value;
2322 const char* format = param->use_decimal ? "%d" : "$%X";
2323
2324 ImGui::Text("%s", param->display_name.c_str());
2325 ImGui::SetNextItemWidth(std::clamp(
2326 ImGui::GetContentRegionAvail().x * 0.50f,
2327 gui::ScaledSize(120.0f, 0.0f).x, gui::ScaledSize(260.0f, 0.0f).x));
2328 if (ImGui::InputInt("##Value", &value, 1, 16)) {
2329 param->value = std::clamp(value, param->min_value, param->max_value);
2330 }
2331
2332 // Show range hint
2333 if (param->min_value != 0 || param->max_value != 0xFF) {
2334 ImGui::SameLine();
2335 ImGui::TextDisabled("(%d-%d)", param->min_value, param->max_value);
2336 }
2337 break;
2338 }
2339
2341 bool checked = (param->value == param->checked_value);
2342 if (ImGui::Checkbox(param->display_name.c_str(), &checked)) {
2343 param->value = checked ? param->checked_value : param->unchecked_value;
2344 }
2345 break;
2346 }
2347
2349 ImGui::Text("%s", param->display_name.c_str());
2350 for (size_t i = 0; i < param->choices.size(); ++i) {
2351 bool selected = (param->value == static_cast<int>(i));
2352 if (ImGui::RadioButton(param->choices[i].c_str(), selected)) {
2353 param->value = static_cast<int>(i);
2354 }
2355 }
2356 break;
2357 }
2358
2360 ImGui::Text("%s", param->display_name.c_str());
2361 for (size_t i = 0; i < param->choices.size(); ++i) {
2362 if (param->choices[i].empty() || param->choices[i] == "_EMPTY") {
2363 continue;
2364 }
2365 bool bit_set = (param->value & (1 << i)) != 0;
2366 if (ImGui::Checkbox(param->choices[i].c_str(), &bit_set)) {
2367 if (bit_set) {
2368 param->value |= (1 << i);
2369 } else {
2370 param->value &= ~(1 << i);
2371 }
2372 }
2373 }
2374 break;
2375 }
2376
2378 ImGui::Text("%s", param->display_name.c_str());
2379 // TODO: Implement item dropdown using game item names
2380 ImGui::SetNextItemWidth(-1.0f);
2381 if (ImGui::InputInt(tr("Item ID"), &param->value)) {
2382 param->value = std::clamp(param->value, 0, 255);
2383 }
2384 break;
2385 }
2386 }
2387
2388 ImGui::PopID();
2389 ImGui::Spacing();
2390}
2391
2392} // namespace editor
2393} // namespace yaze
auto filename() const
Definition rom.h:207
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
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
static Flags & get()
Definition features.h:119
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:254
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)
std::function< project::YazeProject *()> project_provider_
void DrawPatchList(const std::string &folder)
void SetWindowManager(WorkspaceWindowManager *registry)
std::array< char, 128 > settings_search_
void DrawCategoryNavigation(const std::vector< CategoryDescriptor > &categories, bool compact)
ShortcutManager * shortcut_manager_
void EnsureActiveCategory(const std::vector< CategoryDescriptor > &categories)
std::function< std::string()> rom_hash_provider_
void SetDependencies(const EditorDependencies &deps) override
void ApplyNamedLayoutToDockspace(const std::string &name)
bool MatchesShortcutFilter(const std::string &text) const
void DrawParameterWidget(core::PatchParameter *param)
static DungeonOverlaySummary BuildDungeonOverlaySummary(const project::DungeonOverlaySettings &overlay)
core::AsmPatch * selected_patch_
core::PatchManager patch_manager_
OpenMinecartTracksCallback open_minecart_tracks_callback_
project::YazeProject * project_
void SetShortcutManager(ShortcutManager *manager)
void DrawCategoryContent(const std::string &category_id)
void SetUserSettings(UserSettings *settings)
std::array< std::pair< std::string, bool >, 5 > DungeonOverlaySummary
static std::vector< CategoryDescriptor > FilterCategories(Scope scope, const std::string &query)
void SetProject(project::YazeProject *project)
void ApplyDisplayDensity(gui::DensityPreset preset)
absl::Status RequestOpenMinecartTracks()
WorkspaceWindowManager * window_manager_
const Shortcut * FindShortcut(const std::string &name) const
std::vector< Shortcut > GetShortcutsByScope(Shortcut::Scope scope) const
bool ResetShortcutKeys(const std::string &name)
Restore the keys the shortcut was registered with.
std::vector< std::string > FindConflicts(const std::string &name) const
Names of other shortcuts bound to the same chord that can be active at the same time (same editor own...
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:71
Manages user preferences and settings persistence.
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()
void RegisterWidget(const std::string &full_path, const std::string &type, ImGuiID imgui_id, const std::string &description="", const WidgetMetadata &metadata=WidgetMetadata())
static WidgetIdRegistry & Instance()
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_INFO
Definition icons.h:993
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_STORAGE
Definition icons.h:1865
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_LIGHTBULB
Definition icons.h:1083
#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_SCIENCE
Definition icons.h:1656
#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:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
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 AddUniquePath(std::vector< std::string > *paths, const std::string &path)
void DrawShortcutBindingRow(const Shortcut &sc, std::string &value, ShortcutManager *shortcut_manager, UserSettings *user_settings, std::unordered_map< std::string, std::string > &overrides)
constexpr std::array< SettingsPanel::CategoryDescriptor, 10 > kSettingsCategories
std::string BuildHostTagString(const UserSettings::Preferences::AiHost &host)
const char * ScopeLabel(SettingsPanel::Scope scope)
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:140
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
ImVec2 ScaledSize(float base_width, float base_height)
Returns a DPI-scaled version of a fixed pixel size.
Definition ui_config.h:148
DensityPreset
Typography and spacing density presets.
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
Animator & GetAnimator()
Definition animator.cc:318
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
bool FontPicker(const char *label, int *index)
std::string ComputeRomHash(const uint8_t *data, size_t size)
Definition rom_hash.cc:80
void SetPreferHmagicSpriteNames(bool prefer)
Definition sprite.cc:276
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:170
project::YazeProject * project
Definition editor.h:174
ShortcutManager * shortcut_manager
Definition editor.h:186
WorkspaceWindowManager * window_manager
Definition editor.h:182
std::vector< ImGuiKey > keys
std::vector< ImGuiKey > default_keys
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
std::unordered_set< std::string > dismissed_feature_hints
void DrawZSCustomOverworldFlags(Rom *rom)
Dungeon overlay configuration (per-project).
Definition project.h:94
std::vector< uint16_t > track_object_ids
Definition project.h:101
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:102
std::vector< uint16_t > track_stop_tiles
Definition project.h:97
std::vector< uint16_t > track_tiles
Definition project.h:96
std::vector< uint16_t > track_switch_tiles
Definition project.h:98
std::string expected_hash
Definition project.h:224
RomWritePolicy write_policy
Definition project.h:225
std::string rom_backup_folder
Definition project.h:306
std::string git_repository
Definition project.h:374
core::HackManifest hack_manifest
Definition project.h:360
std::string hack_manifest_file
Definition project.h:329
WorkspaceSettings workspace_settings
Definition project.h:346
std::string output_folder
Definition project.h:367
DungeonOverlaySettings dungeon_overlay
Definition project.h:347
std::string symbols_filename
Definition project.h:315
Z3dkSettings z3dk_settings
Definition project.h:363