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", "read_only"};
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 // While a project is open the live preference is its overlay, and project
698 // saves copy the live value back. Keep both in step.
700 user_settings_->prefs().backup_before_save = backup_on_save;
701 }
702 project_->Save();
703 }
704
706 if (gui::DrawProperty("Retention Count", &retention,
707 {.min = 0, .max = 10000})) {
709 project_->Save();
710 }
711
713 if (gui::DrawProperty("Keep Daily Snapshots", &keep_daily)) {
715 project_->Save();
716 }
717
719 if (gui::DrawProperty("Keep Daily Days", &keep_days,
720 {.min = 1, .max = 3650})) {
722 project_->Save();
723 }
724
725 ImGui::Spacing();
726 ImGui::Text(tr("%s Dungeon Overlay"), ICON_MD_TRAIN);
727 ImGui::Separator();
728 ImGui::TextWrapped(
729 tr("Read-only here. Edit collision and object IDs in Minecart Tracks > "
730 "Advanced."));
731
732 constexpr std::array<const char*, 5> kLabels = {
733 "Track Tiles", "Stop Tiles", "Switch Tiles", "Track Object IDs",
734 "Minecart Sprite IDs"};
735 const DungeonOverlaySummary summary =
737 if (ImGui::BeginTable("DungeonOverlaySummary", 2,
738 ImGuiTableFlags_SizingStretchProp)) {
739 ImGui::TableSetupColumn("Field", ImGuiTableColumnFlags_WidthFixed, 132.0f);
740 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
741 for (size_t i = 0; i < summary.size(); ++i) {
742 ImGui::TableNextRow();
743 ImGui::TableSetColumnIndex(0);
744 ImGui::TextDisabled("%s", tr(kLabels[i]));
745 ImGui::TableSetColumnIndex(1);
746 ImGui::TextWrapped("%s", summary[i].first.c_str());
747 ImGui::TextDisabled("%s", summary[i].second
748 ? tr("Standard values")
749 : tr("Custom project values"));
750 }
751 ImGui::EndTable();
752 }
753
754 ImGui::Spacing();
755 const bool custom_objects_enabled =
757 const bool can_open = custom_objects_enabled &&
758 static_cast<bool>(open_minecart_tracks_callback_);
759 ImGui::BeginDisabled(!can_open);
760 if (ImGui::Button(ICON_MD_TRAIN " Open Minecart Tracks")) {
762 }
763 ImGui::EndDisabled();
764
765 if (!custom_objects_enabled) {
766 ImGui::TextDisabled(
767 tr("Enable Custom Dungeon Objects in General Settings first."));
768 } else if (!open_minecart_tracks_callback_) {
769 ImGui::TextDisabled(tr("Minecart Tracks navigation is unavailable."));
770 }
771
772 if (!project_status_message_.empty()) {
773 ImGui::TextColored(gui::GetErrorColor(), "%s",
775 }
776}
777
779 if (!user_settings_) {
780 return;
781 }
782
783 auto& prefs = user_settings_->prefs();
784 auto& roots = prefs.project_root_paths;
785 static int selected_root_index = -1;
786 static std::string new_root_path;
787
788 ImGui::Text(tr("%s Project Roots"), ICON_MD_FOLDER_OPEN);
789 ImGui::Separator();
790
791 if (roots.empty()) {
792 ImGui::TextDisabled(tr("No project roots configured."));
793 }
794
795 const float roots_height = std::clamp(
796 ImGui::GetContentRegionAvail().y * 0.28f, gui::ScaledSize(0.0f, 100.0f).y,
797 gui::ScaledSize(0.0f, 220.0f).y);
798 if (ImGui::BeginChild("ProjectRootsList", ImVec2(0, roots_height), true)) {
799 for (size_t i = 0; i < roots.size(); ++i) {
800 const bool is_default = roots[i] == prefs.default_project_root;
801 std::string label =
803 if (is_default) {
804 label += " (default)";
805 }
806 if (ImGui::Selectable(label.c_str(),
807 selected_root_index == static_cast<int>(i))) {
808 selected_root_index = static_cast<int>(i);
809 }
810 }
811 }
812 ImGui::EndChild();
813
814 const bool has_selection =
815 selected_root_index >= 0 &&
816 selected_root_index < static_cast<int>(roots.size());
817 if (has_selection) {
818 if (ImGui::Button(tr("Set Default"))) {
819 prefs.default_project_root = roots[selected_root_index];
821 }
822 ImGui::SameLine();
823 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
824 const std::string removed = roots[selected_root_index];
825 roots.erase(roots.begin() + selected_root_index);
826 if (prefs.default_project_root == removed) {
827 prefs.default_project_root = roots.empty() ? "" : roots.front();
828 }
829 selected_root_index = roots.empty()
830 ? -1
831 : std::min(selected_root_index,
832 static_cast<int>(roots.size() - 1));
834 }
835 }
836
837 ImGui::Spacing();
838 ImGui::Text(tr("%s Add Root"), ICON_MD_ADD);
839 ImGui::Separator();
840
841 ImGui::InputTextWithHint("##project_root_add", "Add folder path...",
842 &new_root_path);
843 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
844 const std::string trimmed =
845 std::string(absl::StripAsciiWhitespace(new_root_path));
846 if (!trimmed.empty()) {
847 if (AddUniquePath(&roots, trimmed) &&
848 prefs.default_project_root.empty()) {
849 prefs.default_project_root = trimmed;
850 }
852 }
853 }
854 ImGui::SameLine();
855 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
856 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
857 if (!folder.empty()) {
858 if (AddUniquePath(&roots, folder) && prefs.default_project_root.empty()) {
859 prefs.default_project_root = folder;
860 }
862 }
863 }
864
865 ImGui::Spacing();
866 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
867 ImGui::Separator();
868
869 if (ImGui::Button(ICON_MD_HOME " Add Documents")) {
871 if (docs_dir.ok()) {
872 if (AddUniquePath(&roots, docs_dir->string()) &&
873 prefs.default_project_root.empty()) {
874 prefs.default_project_root = docs_dir->string();
875 }
877 }
878 }
879 ImGui::SameLine();
880 if (ImGui::Button(ICON_MD_CLOUD " Add iCloud Projects")) {
881 auto icloud_dir =
883 if (icloud_dir.ok()) {
884 if (AddUniquePath(&roots, icloud_dir->string())) {
885 prefs.default_project_root = icloud_dir->string();
886 }
888 }
889 }
890 ImGui::TextDisabled(
891 tr("iCloud projects live in Documents/Yaze/iCloud on this Mac."));
892
893 ImGui::Spacing();
894 ImGui::Text(tr("%s Sync Options"), ICON_MD_SYNC);
895 ImGui::Separator();
896
897 bool use_icloud_sync = prefs.use_icloud_sync;
898 if (ImGui::Checkbox(tr("Use iCloud sync (Documents)"), &use_icloud_sync)) {
899 prefs.use_icloud_sync = use_icloud_sync;
900 if (use_icloud_sync) {
901 auto icloud_dir =
903 if (icloud_dir.ok()) {
904 AddUniquePath(&roots, icloud_dir->string());
905 prefs.default_project_root = icloud_dir->string();
906 }
907 }
909 }
910
911 bool use_files_app = prefs.use_files_app;
912 if (ImGui::Checkbox(tr("Prefer Files app on iOS"), &use_files_app)) {
913 prefs.use_files_app = use_files_app;
915 }
916}
917
919 auto& theme_manager = gui::ThemeManager::Get();
920 auto theme = theme_manager.GetCurrentTheme();
921 theme.ApplyDensityPreset(preset);
922 // ReapplyTheme, not ApplyTheme: this re-applies the theme the user is
923 // already on, so Classic YAZE has to route back through ColorsYaze().
924 theme_manager.ReapplyTheme(theme);
925}
926
928 auto& theme_manager = gui::ThemeManager::Get();
929
930 ImGui::Text(tr("%s Theme"), ICON_MD_PALETTE);
931 ImGui::Separator();
932
933 const std::string current = theme_manager.GetCurrentThemeName();
934 const auto& current_theme = theme_manager.GetCurrentTheme();
935
936 ImGui::SetNextItemWidth(-1.0f);
937 if (ImGui::BeginCombo("##AppearanceTheme", current.c_str())) {
938 for (const auto& theme_name : theme_manager.GetAvailableThemes()) {
939 const bool is_current = theme_name == current;
940 if (ImGui::Selectable(theme_name.c_str(), is_current)) {
941 if (theme_manager.IsPreviewActive()) {
942 theme_manager.EndPreview();
943 }
944 theme_manager.ApplyTheme(theme_name);
945 }
946 if (is_current) {
947 ImGui::SetItemDefaultFocus();
948 }
949 }
950 ImGui::EndCombo();
951 }
952
953 const ImGuiColorEditFlags swatch_flags = ImGuiColorEditFlags_NoTooltip |
954 ImGuiColorEditFlags_NoDragDrop |
955 ImGuiColorEditFlags_NoPicker;
956 const float swatch_size = std::max(12.0f, ImGui::GetFrameHeight() * 0.62f);
957 ImGui::TextDisabled("%s", tr("Palette"));
958 ImGui::SameLine(0.0f, 6.0f);
959 ImGui::PushID("CurrentThemeSwatches");
960 ImGui::ColorButton("Primary",
961 gui::ConvertColorToImVec4(current_theme.primary),
962 swatch_flags, ImVec2(swatch_size, swatch_size));
963 ImGui::SameLine(0.0f, 3.0f);
964 ImGui::ColorButton("Surface",
965 gui::ConvertColorToImVec4(current_theme.surface),
966 swatch_flags, ImVec2(swatch_size, swatch_size));
967 ImGui::SameLine(0.0f, 3.0f);
968 ImGui::ColorButton("Accent", gui::ConvertColorToImVec4(current_theme.accent),
969 swatch_flags, ImVec2(swatch_size, swatch_size));
970 ImGui::PopID();
971 if (!current_theme.description.empty()) {
972 ImGui::PushTextWrapPos(0.0f);
973 ImGui::TextDisabled("%s", current_theme.description.c_str());
974 ImGui::PopTextWrapPos();
975 }
976
977 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload themes")) {
978 theme_manager.RefreshAvailableThemes();
979 }
980 if (ImGui::IsItemHovered()) {
981 ImGui::SetTooltip(tr("Re-scan theme folders for new or changed themes"));
982 }
983
984 ImGui::Spacing();
985 ImGui::SeparatorText(tr("Density"));
986
987 {
988 auto preset = theme_manager.GetCurrentTheme().density_preset;
989 int density = static_cast<int>(preset);
990 const char* density_labels[] = {tr("Compact"), tr("Normal"),
991 tr("Comfortable")};
992 ImGui::SetNextItemWidth(-1.0f);
993 if (ImGui::Combo("##DisplayDensity", &density, density_labels,
994 IM_ARRAYSIZE(density_labels))) {
995 ApplyDisplayDensity(static_cast<gui::DensityPreset>(density));
996 }
997 ImGui::TextDisabled(
998 "%s", density == 0 ? tr("Tighter controls and more visible content")
999 : density == 2 ? tr("Larger controls with more breathing room")
1000 : tr("Balanced spacing for everyday editing"));
1001 }
1002
1003 ImGui::Spacing();
1004 ImGui::SeparatorText(tr("Motion"));
1005
1006 auto& prefs = user_settings_->prefs();
1007 bool reduced_motion = prefs.reduced_motion;
1008 if (ImGui::Checkbox(tr("Reduced Motion"), &reduced_motion)) {
1009 prefs.reduced_motion = reduced_motion;
1011 prefs.reduced_motion,
1012 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1014 }
1015 if (ImGui::IsItemHovered()) {
1016 ImGui::SetTooltip(
1017 tr("Disable panel/editor transition animations for a calmer editing "
1018 "experience."));
1019 }
1020
1021 int switch_profile = std::clamp(prefs.switch_motion_profile, 0, 2);
1022 const char* switch_profile_labels[] = {"Snappy", "Standard", "Relaxed"};
1023 if (ImGui::Combo(tr("Switch Motion Profile"), &switch_profile,
1024 switch_profile_labels,
1025 IM_ARRAYSIZE(switch_profile_labels))) {
1026 prefs.switch_motion_profile = switch_profile;
1028 prefs.reduced_motion,
1029 gui::Animator::ClampMotionProfile(prefs.switch_motion_profile));
1031 }
1032 if (ImGui::IsItemHovered()) {
1033 ImGui::SetTooltip(
1034 tr("Controls editor/workspace switch timing and easing for panel fades "
1035 "and sidebar slides."));
1036 }
1037
1038 ImGui::Spacing();
1039 ImGui::Text(tr("%s Font"), ICON_MD_TEXT_FIELDS);
1040 ImGui::Separator();
1041
1042 if (user_settings_) {
1043 int font_index = user_settings_->prefs().font_family_index;
1044 if (gui::FontPicker("##font_family", &font_index)) {
1045 user_settings_->prefs().font_family_index = font_index;
1046 ::yaze::SetActiveFontIndex(font_index);
1048 }
1049
1050 ImGui::Text(tr("Global Font Scale"));
1051 float scale = user_settings_->prefs().font_global_scale;
1052 if (ImGui::SliderFloat("##global_font_scale", &scale, 0.5f, 2.0f, "%.2f")) {
1054 ImGui::GetIO().FontGlobalScale = scale;
1056 }
1057 }
1058
1059 ImGui::Spacing();
1060 ImGui::Text(tr("%s Status Bar"), ICON_MD_HORIZONTAL_RULE);
1061 ImGui::Separator();
1062
1063 bool show_status_bar = user_settings_->prefs().show_status_bar;
1064 if (ImGui::Checkbox(tr("Show Status Bar"), &show_status_bar)) {
1065 user_settings_->prefs().show_status_bar = show_status_bar;
1067 // Immediately apply to status bar if status_bar_ is available
1068 if (status_bar_) {
1069 status_bar_->SetEnabled(show_status_bar);
1070 }
1071 }
1072 if (ImGui::IsItemHovered()) {
1073 ImGui::SetTooltip(
1074 tr("Display ROM, session, cursor, and zoom info at bottom of window"));
1075 }
1076
1077 ImGui::Spacing();
1078 ImGui::Text(tr("%s Editor Behavior"), ICON_MD_TUNE);
1079 ImGui::Separator();
1080
1081 bool keep_emu_bg =
1083 if (ImGui::Checkbox(tr("Keep Emulator Running in Background"),
1084 &keep_emu_bg)) {
1087 }
1088 if (ImGui::IsItemHovered()) {
1089 ImGui::SetTooltip(tr(
1090 "When off (default), hiding emulator panels pauses the SNES tick and "
1091 "audio. Music playback still drives its own frames."));
1092 }
1093}
1094
1096 if (!user_settings_) {
1097 ImGui::TextDisabled(tr("UserSettings unavailable."));
1098 return;
1099 }
1100
1101 auto& prefs = user_settings_->prefs();
1102
1103 ImGui::Text(tr("%s Named Layouts"), ICON_MD_DASHBOARD);
1104 ImGui::Separator();
1105
1106 if (prefs.named_layouts.empty()) {
1107 ImGui::TextDisabled(
1108 tr("No saved layouts yet. Open the Layout Designer to capture one."));
1109 } else {
1110 // Sort the keys so the combo order is stable across frames
1111 // (named_layouts is unordered_map).
1112 std::vector<std::string> names;
1113 names.reserve(prefs.named_layouts.size());
1114 for (const auto& entry : prefs.named_layouts) {
1115 names.push_back(entry.first);
1116 }
1117 std::sort(names.begin(), names.end());
1118
1119 const std::string& active = prefs.last_applied_layout_name;
1120 const char* preview =
1121 active.empty() ? "(select a layout…)" : active.c_str();
1122
1123 if (ImGui::BeginCombo(tr("Active Layout"), preview)) {
1124 for (const auto& name : names) {
1125 const bool is_selected = (name == active);
1126 if (ImGui::Selectable(name.c_str(), is_selected)) {
1127 // Mirrors the LayoutManager startup-reapply path in shape:
1128 // lookup → parse → validate → ApplyDockTree. Inlined rather
1129 // than shared because the call sites have different error
1130 // surfacing — this one writes to a transient status string;
1131 // startup writes to the log and a one-shot consumed flag.
1133 }
1134 if (is_selected) {
1135 ImGui::SetItemDefaultFocus();
1136 }
1137 }
1138 ImGui::EndCombo();
1139 }
1140
1141 ImGui::SameLine();
1142 ImGui::BeginDisabled(active.empty() ||
1143 prefs.named_layouts.count(active) == 0);
1144 if (ImGui::SmallButton(tr("Re-apply"))) {
1146 }
1147 ImGui::EndDisabled();
1148 if (ImGui::IsItemHovered()) {
1149 ImGui::SetTooltip(
1150 tr("Re-apply the active layout (useful after closing or rearranging "
1151 "panels manually)."));
1152 }
1153 }
1154
1155 ImGui::Spacing();
1156 if (ImGui::Button(ICON_MD_DASHBOARD_CUSTOMIZE " Open Layout Designer")) {
1157 if (window_manager_ == nullptr) {
1158 workspace_status_message_ = "Designer unavailable: no window manager.";
1160 } else if (!window_manager_->IsWindowOpen("layout.designer") &&
1161 !window_manager_->OpenWindow("layout.designer")) {
1162 // OpenWindow returns false when the panel id isn't registered in
1163 // the active session. Surface that — silently doing nothing on
1164 // a missing registration would hide a real bug (panel renamed,
1165 // force-load archive misconfigured, etc.).
1167 "Open failed: \"layout.designer\" panel is not registered.";
1169 } else {
1170 window_manager_->SetWindowPinned("layout.designer", true);
1173 }
1174 }
1175 if (ImGui::IsItemHovered()) {
1176 ImGui::SetTooltip(
1177 tr("Open the Layout Designer panel to author or capture a workspace "
1178 "layout."));
1179 }
1180
1181 if (!workspace_status_message_.empty()) {
1182 ImGui::Spacing();
1184 ImGui::TextColored(gui::GetErrorColor(), "%s",
1186 } else {
1187 ImGui::TextDisabled("%s", workspace_status_message_.c_str());
1188 }
1189 }
1190}
1191
1192void SettingsPanel::ApplyNamedLayoutToDockspace(const std::string& name) {
1195 if (user_settings_ == nullptr) {
1196 workspace_status_message_ = "Apply failed: UserSettings unavailable.";
1198 return;
1199 }
1200 auto& prefs = user_settings_->prefs();
1201 const auto it = prefs.named_layouts.find(name);
1202 if (it == prefs.named_layouts.end()) {
1204 absl::StrCat("Apply failed: \"", name, "\" not found.");
1206 return;
1207 }
1208
1209 nlohmann::json parsed;
1210 try {
1211 parsed = nlohmann::json::parse(it->second);
1212 } catch (const nlohmann::json::parse_error& e) {
1214 absl::StrCat("Apply failed: invalid JSON (", e.what(), ").");
1216 return;
1217 }
1218
1219 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1220 if (!tree_or.ok()) {
1222 absl::StrCat("Apply failed: ", tree_or.status().message());
1224 return;
1225 }
1226
1227 std::string validation_error;
1228 if (!tree_or->Validate(&validation_error)) {
1230 absl::StrCat("Apply failed: invalid layout (", validation_error, ").");
1232 return;
1233 }
1234
1236 if (manager == nullptr) {
1237 workspace_status_message_ = "Apply failed: LayoutManager unavailable.";
1239 return;
1240 }
1241 const ImGuiID dockspace_id = manager->GetMainDockspaceId();
1242 if (dockspace_id == 0) {
1243 workspace_status_message_ = "Apply failed: main dockspace not yet created.";
1245 return;
1246 }
1247
1248 const absl::Status apply_status =
1249 manager->ApplyDockTree(*tree_or, dockspace_id);
1250 if (!apply_status.ok()) {
1252 absl::StrCat("Apply failed: ", apply_status.message());
1254 return;
1255 }
1256
1257 prefs.last_applied_layout_name = name;
1258 (void)user_settings_->Save();
1259 workspace_status_message_ = absl::StrCat("Applied \"", name, "\".");
1261}
1262
1264 if (!user_settings_)
1265 return;
1266
1267 ImGui::Text(tr("%s Auto-Save"), ICON_MD_SAVE);
1268 ImGui::Separator();
1270 ImGui::TextDisabled(
1271 "%s", tr("These apply to the open project and save with it."));
1272 }
1273
1274 if (ImGui::Checkbox(tr("Enable Auto-Save"),
1277 }
1278
1280 ImGui::Indent();
1281 int interval = static_cast<int>(user_settings_->prefs().autosave_interval);
1282 if (ImGui::SliderInt(tr("Interval (sec)"), &interval, 60, 600)) {
1283 user_settings_->prefs().autosave_interval = static_cast<float>(interval);
1285 }
1286 ImGui::Unindent();
1287 }
1288
1289 // Backups gate every ROM save, not only autosave.
1290 if (ImGui::Checkbox(tr("Backup Before Save"),
1293 }
1294
1295 ImGui::Spacing();
1296 ImGui::Text(tr("%s Recent Files"), ICON_MD_HISTORY);
1297 ImGui::Separator();
1298
1299 if (ImGui::SliderInt(tr("Limit"), &user_settings_->prefs().recent_files_limit,
1300 5, 50)) {
1302 }
1303
1304 ImGui::Spacing();
1305 ImGui::Text(tr("%s Default Editor"), ICON_MD_EDIT);
1306 ImGui::Separator();
1307
1308 // Index order is persisted as default_editor; append new editors only.
1309 const char* editors[] = {"None", "Overworld", "Dungeon", "Graphics",
1310 "Messages"};
1311 if (ImGui::Combo("##DefaultEditor", &user_settings_->prefs().default_editor,
1312 editors, IM_ARRAYSIZE(editors))) {
1314 }
1315 ImGui::TextDisabled(
1316 "%s", tr("Opens after a ROM or project loads. None shows the picker."));
1317
1318 ImGui::Spacing();
1319 ImGui::Text(tr("%s Sprite Names"), ICON_MD_LABEL);
1320 ImGui::Separator();
1321 if (ImGui::Checkbox(tr("Use HMagic sprite names (expanded)"),
1326 }
1327
1328 ImGui::Spacing();
1329 ImGui::Text(tr("%s Testing"), ICON_MD_SCIENCE);
1330 ImGui::Separator();
1331 if (ImGui::Checkbox(tr("Test mode"), &user_settings_->prefs().test_mode)) {
1333 }
1334 if (ImGui::IsItemHovered()) {
1335 ImGui::SetTooltip(
1336 tr("From the next launch: no welcome screen, no editor picker, and "
1337 "the local test server starts on port 50052 (builds with gRPC)."));
1338 }
1339
1340 ImGui::Spacing();
1341 ImGui::Text(tr("%s Onboarding Tips"), ICON_MD_LIGHTBULB);
1342 ImGui::Separator();
1343 if (ImGui::Checkbox(tr("Show feature tips (ZScream migration, etc.)"),
1346 }
1347 ImGui::TextDisabled(tr("%zu tip(s) already dismissed"),
1349 if (ImGui::Button(tr("Reset dismissed tips"))) {
1352 }
1353}
1354
1356 if (!user_settings_)
1357 return;
1358
1359 ImGui::Text(tr("%s Graphics"), ICON_MD_IMAGE);
1360 ImGui::Separator();
1361
1362 if (ImGui::Checkbox(tr("V-Sync"), &user_settings_->prefs().vsync)) {
1364 }
1365
1366 if (ImGui::SliderInt(tr("Target FPS"), &user_settings_->prefs().target_fps,
1367 30, 144)) {
1369 }
1370
1371 ImGui::Spacing();
1372 ImGui::Text(tr("%s Memory"), ICON_MD_MEMORY);
1373 ImGui::Separator();
1374
1375 if (ImGui::SliderInt(tr("Cache Size (MB)"),
1376 &user_settings_->prefs().cache_size_mb, 128, 2048)) {
1378 }
1379
1380 if (ImGui::SliderInt(tr("Undo History"),
1381 &user_settings_->prefs().undo_history_size, 10, 200)) {
1383 }
1384
1385 ImGui::Spacing();
1386 ImGui::Separator();
1387 ImGui::Text(tr("Current FPS: %.1f"), ImGui::GetIO().Framerate);
1388 ImGui::Text(tr("Frame Time: %.3f ms"), 1000.0f / ImGui::GetIO().Framerate);
1389}
1390
1392 if (!user_settings_)
1393 return;
1394
1395 auto& prefs = user_settings_->prefs();
1396 auto& hosts = prefs.ai_hosts;
1397 static int selected_host_index = -1;
1398
1399 auto draw_key_row = [&](const char* label, std::string* key,
1400 const char* env_var, const char* id) {
1401 ImGui::PushID(id);
1402 ImGui::Text("%s", label);
1403 const ImVec2 button_size = ImGui::CalcTextSize(ICON_MD_SYNC " Env");
1404 float env_button_width =
1405 button_size.x + ImGui::GetStyle().FramePadding.x * 2.0f;
1406 float input_width = ImGui::GetContentRegionAvail().x - env_button_width -
1407 ImGui::GetStyle().ItemSpacing.x;
1408 bool stack = input_width < 160.0f;
1409 ImGui::SetNextItemWidth(stack ? -1.0f : input_width);
1410 if (ImGui::InputTextWithHint("##key", "API key...", key,
1411 ImGuiInputTextFlags_Password)) {
1413 }
1414 if (!stack) {
1415 ImGui::SameLine();
1416 }
1417 if (ImGui::SmallButton(ICON_MD_SYNC " Env")) {
1418 const char* env_key = std::getenv(env_var);
1419 if (env_key) {
1420 *key = env_key;
1422 }
1423 }
1424 ImGui::Spacing();
1425 ImGui::PopID();
1426 };
1427
1428 ImGui::Text(tr("%s Provider Keys"), ICON_MD_VPN_KEY);
1429 ImGui::Separator();
1430 draw_key_row("OpenAI", &prefs.openai_api_key, "OPENAI_API_KEY", "openai_key");
1431 draw_key_row("Anthropic", &prefs.anthropic_api_key, "ANTHROPIC_API_KEY",
1432 "anthropic_key");
1433 draw_key_row("Google (Gemini)", &prefs.gemini_api_key, "GEMINI_API_KEY",
1434 "gemini_key");
1435 ImGui::Spacing();
1436
1437 // Provider selection
1438 ImGui::Text(tr("%s Provider Defaults (legacy)"), ICON_MD_CLOUD);
1439 ImGui::Separator();
1440
1441 const char* providers[] = {"Ollama (Local)", "Gemini (Cloud)",
1442 "Mock (Testing)"};
1443 if (ImGui::Combo("##Provider", &prefs.ai_provider, providers,
1444 IM_ARRAYSIZE(providers))) {
1446 }
1447
1448 ImGui::Spacing();
1449 ImGui::Text(tr("%s Host Routing"), ICON_MD_STORAGE);
1450 ImGui::Separator();
1451
1452 const char* active_preview = "None";
1453 const char* remote_preview = "None";
1454 for (const auto& host : hosts) {
1455 if (!prefs.active_ai_host_id.empty() &&
1456 host.id == prefs.active_ai_host_id) {
1457 active_preview = host.label.c_str();
1458 }
1459 if (!prefs.remote_build_host_id.empty() &&
1460 host.id == prefs.remote_build_host_id) {
1461 remote_preview = host.label.c_str();
1462 }
1463 }
1464
1465 if (ImGui::BeginCombo(tr("Active Host"), active_preview)) {
1466 for (size_t i = 0; i < hosts.size(); ++i) {
1467 const bool is_selected = (!prefs.active_ai_host_id.empty() &&
1468 hosts[i].id == prefs.active_ai_host_id);
1469 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1470 prefs.active_ai_host_id = hosts[i].id;
1471 if (prefs.remote_build_host_id.empty()) {
1472 prefs.remote_build_host_id = hosts[i].id;
1473 }
1475 }
1476 if (is_selected) {
1477 ImGui::SetItemDefaultFocus();
1478 }
1479 }
1480 ImGui::EndCombo();
1481 }
1482
1483 if (ImGui::BeginCombo(tr("Remote Build Host"), remote_preview)) {
1484 for (size_t i = 0; i < hosts.size(); ++i) {
1485 const bool is_selected = (!prefs.remote_build_host_id.empty() &&
1486 hosts[i].id == prefs.remote_build_host_id);
1487 if (ImGui::Selectable(hosts[i].label.c_str(), is_selected)) {
1488 prefs.remote_build_host_id = hosts[i].id;
1490 }
1491 if (is_selected) {
1492 ImGui::SetItemDefaultFocus();
1493 }
1494 }
1495 ImGui::EndCombo();
1496 }
1497
1498 ImGui::Spacing();
1499 ImGui::Text(tr("%s AI Hosts"), ICON_MD_STORAGE);
1500 ImGui::Separator();
1501
1502 if (selected_host_index >= static_cast<int>(hosts.size())) {
1503 selected_host_index = hosts.empty() ? -1 : 0;
1504 }
1505 if (selected_host_index < 0 && !hosts.empty()) {
1506 for (size_t i = 0; i < hosts.size(); ++i) {
1507 if (!prefs.active_ai_host_id.empty() &&
1508 hosts[i].id == prefs.active_ai_host_id) {
1509 selected_host_index = static_cast<int>(i);
1510 break;
1511 }
1512 }
1513 if (selected_host_index < 0) {
1514 selected_host_index = 0;
1515 }
1516 }
1517
1518 const float hosts_height = std::clamp(
1519 ImGui::GetContentRegionAvail().y * 0.32f, gui::ScaledSize(0.0f, 120.0f).y,
1520 gui::ScaledSize(0.0f, 240.0f).y);
1521 ImGui::BeginChild("##ai_host_list", ImVec2(0, hosts_height), true);
1522 for (size_t i = 0; i < hosts.size(); ++i) {
1523 const bool is_selected = static_cast<int>(i) == selected_host_index;
1524 std::string label = hosts[i].label;
1525 if (hosts[i].id == prefs.active_ai_host_id) {
1526 label += " (active)";
1527 }
1528 if (hosts[i].id == prefs.remote_build_host_id) {
1529 label += " (build)";
1530 }
1531 if (ImGui::Selectable(label.c_str(), is_selected)) {
1532 selected_host_index = static_cast<int>(i);
1533 }
1534 std::string tags = BuildHostTagString(hosts[i]);
1535 if (!tags.empty()) {
1536 ImGui::SameLine();
1537 ImGui::TextDisabled("%s", tags.c_str());
1538 }
1539 }
1540 ImGui::EndChild();
1541
1542 auto add_host = [&](UserSettings::Preferences::AiHost host) {
1543 if (host.id.empty()) {
1544 host.id = absl::StrFormat("host-%zu", hosts.size() + 1);
1545 }
1546 hosts.push_back(host);
1547 selected_host_index = static_cast<int>(hosts.size() - 1);
1548 if (prefs.active_ai_host_id.empty()) {
1549 prefs.active_ai_host_id = host.id;
1550 }
1551 if (prefs.remote_build_host_id.empty()) {
1552 prefs.remote_build_host_id = host.id;
1553 }
1555 };
1556
1557 if (ImGui::Button(ICON_MD_ADD " Add Host")) {
1559 host.label = "New Host";
1560 host.base_url = "http://localhost:1234";
1561 host.api_type = "openai";
1562 add_host(host);
1563 }
1564 ImGui::SameLine();
1565 if (ImGui::Button(ICON_MD_DELETE " Remove") && selected_host_index >= 0 &&
1566 selected_host_index < static_cast<int>(hosts.size())) {
1567 const std::string removed_id = hosts[selected_host_index].id;
1568 hosts.erase(hosts.begin() + selected_host_index);
1569 if (prefs.active_ai_host_id == removed_id) {
1570 prefs.active_ai_host_id = hosts.empty() ? "" : hosts.front().id;
1571 }
1572 if (prefs.remote_build_host_id == removed_id) {
1573 prefs.remote_build_host_id = prefs.active_ai_host_id;
1574 }
1575 selected_host_index =
1576 hosts.empty()
1577 ? -1
1578 : std::min(selected_host_index, static_cast<int>(hosts.size() - 1));
1580 }
1581
1582 ImGui::SameLine();
1583 if (ImGui::Button(tr("Add LM Studio"))) {
1585 host.label = "LM Studio (local)";
1586 host.base_url = "http://localhost:1234";
1588 host.supports_tools = true;
1589 host.supports_streaming = true;
1590 add_host(host);
1591 }
1592 ImGui::SameLine();
1593 if (ImGui::Button(tr("Add AFS Bridge"))) {
1595 host.label = "halext AFS Bridge";
1596 host.base_url = "https://halext.org";
1598 host.supports_tools = true;
1599 host.supports_streaming = true;
1600 add_host(host);
1601 }
1602 ImGui::SameLine();
1603 if (ImGui::Button(tr("Add Ollama"))) {
1605 host.label = "Ollama (local)";
1606 host.base_url = "http://localhost:11434";
1608 host.supports_tools = true;
1609 host.supports_streaming = true;
1610 add_host(host);
1611 }
1612
1613 static std::string tailscale_host;
1614 ImGui::InputTextWithHint("##tailscale_host", "host.ts.net:1234",
1615 &tailscale_host);
1616 ImGui::SameLine();
1617 if (ImGui::Button(tr("Add Tailscale Host"))) {
1618 std::string trimmed =
1619 std::string(absl::StripAsciiWhitespace(tailscale_host));
1620 if (!trimmed.empty()) {
1622 host.label = "Tailscale Host";
1623 if (absl::StrContains(trimmed, "://")) {
1624 host.base_url = trimmed;
1625 } else {
1626 host.base_url = "http://" + trimmed;
1627 }
1629 host.supports_tools = true;
1630 host.supports_streaming = true;
1631 host.allow_insecure = true;
1632 add_host(host);
1633 tailscale_host.clear();
1634 }
1635 }
1636
1637 if (selected_host_index >= 0 &&
1638 selected_host_index < static_cast<int>(hosts.size())) {
1639 auto& host = hosts[static_cast<size_t>(selected_host_index)];
1640 ImGui::Spacing();
1641 ImGui::Text(tr("Host Details"));
1642 ImGui::Separator();
1643 if (ImGui::InputText(tr("Label"), &host.label)) {
1645 }
1646 if (ImGui::InputText(tr("Base URL"), &host.base_url)) {
1648 }
1649
1650 const char* api_types[] = {"openai", "ollama", "gemini",
1651 "anthropic", "lmstudio", "grpc"};
1652 int api_index = 0;
1653 for (int i = 0; i < IM_ARRAYSIZE(api_types); ++i) {
1654 if (host.api_type == api_types[i]) {
1655 api_index = i;
1656 break;
1657 }
1658 }
1659 if (ImGui::Combo(tr("API Type"), &api_index, api_types,
1660 IM_ARRAYSIZE(api_types))) {
1661 host.api_type = api_types[api_index];
1663 }
1664
1665 if (ImGui::InputText(tr("API Key"), &host.api_key,
1666 ImGuiInputTextFlags_Password)) {
1668 }
1669 if (ImGui::InputText(tr("Keychain ID"), &host.credential_id)) {
1671 }
1672 ImGui::SameLine();
1673 if (ImGui::SmallButton(tr("Use Host ID"))) {
1674 host.credential_id = host.id;
1676 }
1677 if (!host.credential_id.empty() && host.api_key.empty()) {
1678 ImGui::TextDisabled(tr("Keychain lookup enabled (leave API key empty)."));
1679 }
1680
1681 if (ImGui::Checkbox(tr("Supports Vision"), &host.supports_vision)) {
1683 }
1684 ImGui::SameLine();
1685 if (ImGui::Checkbox(tr("Supports Tools"), &host.supports_tools)) {
1687 }
1688 ImGui::SameLine();
1689 if (ImGui::Checkbox(tr("Supports Streaming"), &host.supports_streaming)) {
1691 }
1692 if (ImGui::Checkbox(tr("Allow Insecure HTTP"), &host.allow_insecure)) {
1694 }
1695 }
1696
1697 ImGui::Spacing();
1698 ImGui::Text(tr("%s Local Model Paths"), ICON_MD_FOLDER);
1699 ImGui::Separator();
1700
1701 auto& model_paths = prefs.ai_model_paths;
1702 static int selected_model_path = -1;
1703 static std::string new_model_path;
1704
1705 if (model_paths.empty()) {
1706 ImGui::TextDisabled(tr("No model paths configured."));
1707 }
1708
1709 const float model_paths_height = std::clamp(
1710 ImGui::GetContentRegionAvail().y * 0.24f, gui::ScaledSize(0.0f, 96.0f).y,
1711 gui::ScaledSize(0.0f, 180.0f).y);
1712 if (ImGui::BeginChild("ModelPathsList", ImVec2(0, model_paths_height),
1713 true)) {
1714 for (size_t i = 0; i < model_paths.size(); ++i) {
1715 std::string label =
1717 if (ImGui::Selectable(label.c_str(),
1718 selected_model_path == static_cast<int>(i))) {
1719 selected_model_path = static_cast<int>(i);
1720 }
1721 }
1722 }
1723 ImGui::EndChild();
1724
1725 const bool has_model_selection =
1726 selected_model_path >= 0 &&
1727 selected_model_path < static_cast<int>(model_paths.size());
1728 if (has_model_selection) {
1729 if (ImGui::Button(ICON_MD_DELETE " Remove")) {
1730 model_paths.erase(model_paths.begin() + selected_model_path);
1731 selected_model_path =
1732 model_paths.empty()
1733 ? -1
1734 : std::min(selected_model_path,
1735 static_cast<int>(model_paths.size() - 1));
1737 }
1738 }
1739
1740 ImGui::Spacing();
1741 ImGui::InputTextWithHint("##model_path_add", "Add folder path...",
1742 &new_model_path);
1743 if (ImGui::Button(ICON_MD_ADD " Add Path")) {
1744 const std::string trimmed =
1745 std::string(absl::StripAsciiWhitespace(new_model_path));
1746 if (!trimmed.empty() && AddUniquePath(&model_paths, trimmed)) {
1748 new_model_path.clear();
1749 }
1750 }
1751 ImGui::SameLine();
1752 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Browse")) {
1753 const std::string folder = util::FileDialogWrapper::ShowOpenFolderDialog();
1754 if (!folder.empty() && AddUniquePath(&model_paths, folder)) {
1756 }
1757 }
1758
1759 ImGui::Spacing();
1760 ImGui::Text(tr("%s Quick Add"), ICON_MD_BOLT);
1761 ImGui::Separator();
1762 const auto home_dir = util::PlatformPaths::GetHomeDirectory();
1763 if (ImGui::Button(ICON_MD_HOME " Add ~/models")) {
1764 if (!home_dir.empty() && home_dir != ".") {
1765 if (AddUniquePath(&model_paths, (home_dir / "models").string())) {
1767 }
1768 }
1769 }
1770 ImGui::SameLine();
1771 if (ImGui::Button(tr("Add ~/.lmstudio/models"))) {
1772 if (!home_dir.empty() && home_dir != ".") {
1773 if (AddUniquePath(&model_paths,
1774 (home_dir / ".lmstudio" / "models").string())) {
1776 }
1777 }
1778 }
1779 ImGui::SameLine();
1780 if (ImGui::Button(tr("Add ~/.ollama/models"))) {
1781 if (!home_dir.empty() && home_dir != ".") {
1782 if (AddUniquePath(&model_paths,
1783 (home_dir / ".ollama" / "models").string())) {
1785 }
1786 }
1787 }
1788
1789 ImGui::Spacing();
1790 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
1791 ImGui::Separator();
1792
1793 if (ImGui::SliderFloat(tr("Temperature"),
1794 &user_settings_->prefs().ai_temperature, 0.0f, 2.0f)) {
1796 }
1797 ImGui::TextDisabled(tr("Higher = more creative"));
1798
1799 if (ImGui::SliderInt(tr("Max Tokens"), &user_settings_->prefs().ai_max_tokens,
1800 256, 8192)) {
1802 }
1803
1804 ImGui::Spacing();
1805 ImGui::Text(tr("%s Behavior"), ICON_MD_PSYCHOLOGY);
1806 ImGui::Separator();
1807
1808 if (ImGui::Checkbox(tr("Proactive Suggestions"),
1811 }
1812
1813 if (ImGui::Checkbox(tr("Auto-Learn Preferences"),
1816 }
1817
1818 if (ImGui::Checkbox(tr("Enable Vision"),
1821 }
1822
1823 ImGui::Spacing();
1824 ImGui::Text(tr("%s Logging"), ICON_MD_TERMINAL);
1825 ImGui::Separator();
1826
1827 const char* log_levels[] = {"Debug", "Info", "Warning", "Error", "Fatal"};
1828 if (ImGui::Combo(tr("Log Level"), &user_settings_->prefs().log_level,
1829 log_levels, IM_ARRAYSIZE(log_levels))) {
1830 // Apply log level logic here if needed
1832 }
1833}
1834
1836 if (ImGui::TreeNodeEx(ICON_MD_KEYBOARD " Shortcuts",
1837 ImGuiTreeNodeFlags_DefaultOpen)) {
1838 ImGui::InputTextWithHint("##shortcut_filter", "Filter shortcuts...",
1840 if (ImGui::IsItemHovered()) {
1841 ImGui::SetTooltip(tr("Filter by action name or key combo"));
1842 }
1843 ImGui::Spacing();
1844
1845 if (ImGui::TreeNode("Global Shortcuts")) {
1847 ImGui::TreePop();
1848 }
1849 if (ImGui::TreeNode("Editor Shortcuts")) {
1851 ImGui::TreePop();
1852 }
1853 if (ImGui::TreeNode("Panel Shortcuts")) {
1855 ImGui::TreePop();
1856 }
1857 ImGui::TextDisabled(
1858 tr("Tip: Use Cmd/Opt labels on macOS or Ctrl/Alt on Windows/Linux. "
1859 "Function keys and symbols (/, -) are supported."));
1860 ImGui::TreePop();
1861 }
1862}
1863
1864bool SettingsPanel::MatchesShortcutFilter(const std::string& text) const {
1865 if (shortcut_filter_.empty()) {
1866 return true;
1867 }
1868 std::string haystack = absl::AsciiStrToLower(text);
1869 std::string needle = absl::AsciiStrToLower(shortcut_filter_);
1870 return absl::StrContains(haystack, needle);
1871}
1872
1873namespace {
1874
1875// One editable binding row: text field (Enter applies), conflict warning, and
1876// a reset button when the binding differs from its registered default.
1877// `overrides` is the persisted map for this scope (global_shortcuts or
1878// editor_shortcuts): absent = default, "" = explicitly unbound.
1880 const Shortcut& sc, std::string& value, ShortcutManager* shortcut_manager,
1881 UserSettings* user_settings,
1882 std::unordered_map<std::string, std::string>& overrides) {
1883 ImGui::PushID(sc.name.c_str());
1884 ImGui::Text("%s", sc.name.c_str());
1885 ImGui::SameLine();
1886 ImGui::SetNextItemWidth(std::clamp(ImGui::GetContentRegionAvail().x * 0.45f,
1887 gui::ScaledSize(160.0f, 0.0f).x,
1888 gui::ScaledSize(300.0f, 0.0f).x));
1889 if (ImGui::InputText("##binding", &value,
1890 ImGuiInputTextFlags_EnterReturnsTrue |
1891 ImGuiInputTextFlags_AutoSelectAll)) {
1892 auto parsed = ParseShortcut(value);
1893 if (!parsed.empty() || value.empty()) {
1894 // Empty string unbinds the shortcut (persisted as "").
1895 shortcut_manager->UpdateShortcutKeys(sc.name, parsed);
1896 overrides[sc.name] = value;
1897 value = PrintShortcut(parsed);
1898 user_settings->Save();
1899 }
1900 }
1901
1902 const Shortcut* live = shortcut_manager->FindShortcut(sc.name);
1903 if (live) {
1904 const auto conflicts = shortcut_manager->FindConflicts(sc.name);
1905 if (!conflicts.empty()) {
1906 ImGui::SameLine();
1907 ImGui::TextColored(gui::GetWarningColor(), ICON_MD_WARNING);
1908 if (ImGui::IsItemHovered()) {
1909 std::string tip = "Also bound to:";
1910 for (const auto& other : conflicts) {
1911 tip += "\n " + other;
1912 }
1913 ImGui::SetTooltip("%s", tip.c_str());
1914 }
1915 }
1916 if (live->keys != live->default_keys) {
1917 ImGui::SameLine();
1918 if (ImGui::SmallButton(tr("Reset"))) {
1919 shortcut_manager->ResetShortcutKeys(sc.name);
1920 overrides.erase(sc.name);
1921 value = PrintShortcut(live->default_keys);
1922 user_settings->Save();
1923 }
1924 if (ImGui::IsItemHovered()) {
1925 ImGui::SetTooltip("Default: %s",
1926 live->default_keys.empty()
1927 ? "(none)"
1928 : PrintShortcut(live->default_keys).c_str());
1929 }
1930 }
1931 }
1932 ImGui::PopID();
1933}
1934
1935} // namespace
1936
1939 ImGui::TextDisabled(tr("Not available"));
1940 return;
1941 }
1942
1943 auto shortcuts =
1945 if (shortcuts.empty()) {
1946 ImGui::TextDisabled(tr("No global shortcuts registered."));
1947 return;
1948 }
1949 std::sort(
1950 shortcuts.begin(), shortcuts.end(),
1951 [](const Shortcut& a, const Shortcut& b) { return a.name < b.name; });
1952
1953 static std::unordered_map<std::string, std::string> editing;
1954
1955 bool has_match = false;
1956 for (const auto& sc : shortcuts) {
1957 if (sc.keys.empty() && sc.default_keys.empty()) {
1958 continue; // Command-palette-only entry; nothing to bind here.
1959 }
1960 std::string keys = PrintShortcut(sc.keys);
1961 if (!MatchesShortcutFilter(sc.name) && !MatchesShortcutFilter(keys)) {
1962 continue;
1963 }
1964 has_match = true;
1965 auto it = editing.find(sc.name);
1966 if (it == editing.end()) {
1967 it = editing.emplace(sc.name, keys).first;
1968 }
1969 DrawShortcutBindingRow(sc, it->second, shortcut_manager_, user_settings_,
1971 }
1972 if (!has_match) {
1973 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
1974 }
1975}
1976
1979 ImGui::TextDisabled(tr("Not available"));
1980 return;
1981 }
1982
1983 auto shortcuts =
1985 std::map<std::string, std::vector<Shortcut>> grouped;
1986 static std::unordered_map<std::string, std::string> editing;
1987
1988 for (const auto& sc : shortcuts) {
1989 if (sc.keys.empty() && sc.default_keys.empty()) {
1990 continue; // Command-palette-only entry; nothing to bind here.
1991 }
1992 auto pos = sc.name.find(".");
1993 std::string group =
1994 pos != std::string::npos ? sc.name.substr(0, pos) : "general";
1995 grouped[group].push_back(sc);
1996 }
1997 bool has_match = false;
1998 for (auto& [group, list] : grouped) {
1999 std::sort(
2000 list.begin(), list.end(),
2001 [](const Shortcut& a, const Shortcut& b) { return a.name < b.name; });
2002 std::vector<Shortcut> filtered;
2003 filtered.reserve(list.size());
2004 for (const auto& sc : list) {
2005 std::string keys = PrintShortcut(sc.keys);
2006 if (MatchesShortcutFilter(sc.name) || MatchesShortcutFilter(keys)) {
2007 filtered.push_back(sc);
2008 }
2009 }
2010 if (filtered.empty()) {
2011 continue;
2012 }
2013 has_match = true;
2014 if (ImGui::TreeNode(group.c_str())) {
2015 for (const auto& sc : filtered) {
2016 auto it = editing.find(sc.name);
2017 if (it == editing.end()) {
2018 it = editing.emplace(sc.name, PrintShortcut(sc.keys)).first;
2019 }
2020 DrawShortcutBindingRow(sc, it->second, shortcut_manager_,
2023 }
2024 ImGui::TreePop();
2025 }
2026 }
2027 if (!has_match) {
2028 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
2029 }
2030}
2031
2034 ImGui::TextDisabled(tr("Registry not available"));
2035 return;
2036 }
2037
2038 // Simplified shortcut editor for sidebar
2039 auto categories = window_manager_->GetAllCategories();
2040
2041 bool has_match = false;
2042 for (const auto& category : categories) {
2043 auto cards = window_manager_->GetWindowsInCategory(0, category);
2044 std::vector<decltype(cards)::value_type> filtered_cards;
2045 filtered_cards.reserve(cards.size());
2046 for (const auto& card : cards) {
2047 if (MatchesShortcutFilter(card.display_name) ||
2048 MatchesShortcutFilter(card.card_id)) {
2049 filtered_cards.push_back(card);
2050 }
2051 }
2052 if (filtered_cards.empty()) {
2053 continue;
2054 }
2055 has_match = true;
2056 if (ImGui::TreeNode(category.c_str())) {
2057
2058 for (const auto& card : filtered_cards) {
2059 ImGui::PushID(card.card_id.c_str());
2060
2061 ImGui::Text("%s %s", card.icon.c_str(), card.display_name.c_str());
2062
2063 std::string current_shortcut;
2064 auto it = user_settings_->prefs().panel_shortcuts.find(card.card_id);
2065 if (it != user_settings_->prefs().panel_shortcuts.end()) {
2066 current_shortcut = it->second;
2067 } else if (!card.shortcut_hint.empty()) {
2068 current_shortcut = card.shortcut_hint;
2069 } else {
2070 current_shortcut = "None";
2071 }
2072
2073 // Display platform-aware label
2074 std::string display_shortcut = current_shortcut;
2075 auto parsed = ParseShortcut(current_shortcut);
2076 if (!parsed.empty()) {
2077 display_shortcut = PrintShortcut(parsed);
2078 }
2079
2080 if (is_editing_shortcut_ && editing_card_id_ == card.card_id) {
2081 ImGui::SetNextItemWidth(
2082 std::clamp(ImGui::GetContentRegionAvail().x * 0.40f,
2083 gui::ScaledSize(100.0f, 0.0f).x,
2084 gui::ScaledSize(220.0f, 0.0f).x));
2085 ImGui::SetKeyboardFocusHere();
2086 if (ImGui::InputText("##Edit", shortcut_edit_buffer_,
2087 sizeof(shortcut_edit_buffer_),
2088 ImGuiInputTextFlags_EnterReturnsTrue)) {
2089 if (strlen(shortcut_edit_buffer_) > 0) {
2090 user_settings_->prefs().panel_shortcuts[card.card_id] =
2092 } else {
2093 user_settings_->prefs().panel_shortcuts.erase(card.card_id);
2094 }
2096 is_editing_shortcut_ = false;
2097 editing_card_id_.clear();
2098 }
2099 ImGui::SameLine();
2100 if (ImGui::Button(ICON_MD_CLOSE)) {
2101 is_editing_shortcut_ = false;
2102 editing_card_id_.clear();
2103 }
2104 } else {
2105 if (ImGui::Button(display_shortcut.c_str(), ImVec2(120, 0))) {
2106 is_editing_shortcut_ = true;
2107 editing_card_id_ = card.card_id;
2108 strncpy(shortcut_edit_buffer_, current_shortcut.c_str(),
2109 sizeof(shortcut_edit_buffer_) - 1);
2110 }
2111 if (ImGui::IsItemHovered()) {
2112 ImGui::SetTooltip(tr("Click to edit shortcut"));
2113 }
2114 }
2115
2116 ImGui::PopID();
2117 }
2118
2119 ImGui::TreePop();
2120 }
2121 }
2122 if (!has_match) {
2123 ImGui::TextDisabled(tr("No shortcuts match the current filter."));
2124 }
2125}
2126
2128 // Load patches on first access
2129 if (!patches_loaded_) {
2130 // Try to load from default patches location
2131 auto patches_dir_status = util::PlatformPaths::FindAsset("patches");
2132 if (patches_dir_status.ok()) {
2133 auto status = patch_manager_.LoadPatches(patches_dir_status->string());
2134 if (status.ok()) {
2135 patches_loaded_ = true;
2136 if (!patch_manager_.folders().empty()) {
2138 }
2139 }
2140 }
2141 }
2142
2143 ImGui::Text(tr("%s ZScream Patch System"), ICON_MD_EXTENSION);
2144 ImGui::Separator();
2145
2146 if (!patches_loaded_) {
2147 ImGui::TextDisabled(tr("No patches loaded"));
2148 ImGui::TextDisabled(tr("Place .asm patches in assets/patches/"));
2149
2150 if (ImGui::Button(tr("Browse for Patches Folder..."))) {
2151 // TODO: File browser for patches folder
2152 }
2153 return;
2154 }
2155
2156 // Status line
2157 int enabled_count = patch_manager_.GetEnabledPatchCount();
2158 int total_count = static_cast<int>(patch_manager_.patches().size());
2159 ImGui::Text(tr("Loaded: %d patches (%d enabled)"), total_count,
2160 enabled_count);
2161
2162 ImGui::Spacing();
2163
2164 // Folder tabs
2165 if (gui::BeginThemedTabBar("##PatchFolders",
2166 ImGuiTabBarFlags_FittingPolicyScroll)) {
2167 for (const auto& folder : patch_manager_.folders()) {
2168 if (ImGui::BeginTabItem(folder.c_str())) {
2169 selected_folder_ = folder;
2170 DrawPatchList(folder);
2171 ImGui::EndTabItem();
2172 }
2173 }
2175 }
2176
2177 ImGui::Spacing();
2178 ImGui::Separator();
2179
2180 // Selected patch details
2181 if (selected_patch_) {
2183 } else {
2184 ImGui::TextDisabled(tr("Select a patch to view details"));
2185 }
2186
2187 ImGui::Spacing();
2188 ImGui::Separator();
2189
2190 // Action buttons
2191 if (ImGui::Button(ICON_MD_CHECK " Apply Patches to ROM")) {
2192 if (rom_ && rom_->is_loaded()) {
2193#ifdef YAZE_WITH_Z3DK
2194 if (project_) {
2196 const auto& z3dk = project_->z3dk_settings;
2197 options.include_paths = z3dk.include_paths;
2198 options.defines = z3dk.defines;
2199 options.std_includes_path = z3dk.std_includes_path;
2200 options.std_defines_path = z3dk.std_defines_path;
2201 options.mapper = z3dk.mapper;
2202 options.rom_size = z3dk.rom_size;
2203 options.capture_nocash_symbols = (z3dk.symbols_format == "nocash");
2204 options.warn_unused_symbols = z3dk.warn_unused_symbols;
2205 options.warn_branch_outside_bank = z3dk.warn_branch_outside_bank;
2206 options.warn_unknown_width = z3dk.warn_unknown_width;
2207 options.warn_org_collision = z3dk.warn_org_collision;
2208 options.warn_unauthorized_hook = z3dk.warn_unauthorized_hook;
2209 options.warn_stack_balance = z3dk.warn_stack_balance;
2210 options.warn_hook_return = z3dk.warn_hook_return;
2211 for (const auto& range : z3dk.prohibited_memory_ranges) {
2212 options.prohibited_memory_ranges.push_back(
2213 {.start = range.start, .end = range.end, .reason = range.reason});
2214 }
2215 if (!project_->code_folder.empty() &&
2216 std::find(options.include_paths.begin(),
2217 options.include_paths.end(),
2218 project_->code_folder) == options.include_paths.end()) {
2219 options.include_paths.push_back(project_->code_folder);
2220 }
2221 if (!z3dk.rom_path.empty()) {
2222 options.hooks_rom_path = z3dk.rom_path;
2223 }
2224 if (!rom_->filename().empty()) {
2225 options.hooks_rom_path = rom_->filename();
2226 }
2227 patch_manager_.SetZ3dkAssembleOptions(options);
2228 }
2229#endif
2231 if (!status.ok()) {
2232 LOG_ERROR("Settings", "Failed to apply patches: %s", status.message());
2233 } else {
2234 LOG_INFO("Settings", "Applied %d patches successfully", enabled_count);
2235 }
2236 } else {
2237 LOG_WARN("Settings", "No ROM loaded");
2238 }
2239 }
2240 if (ImGui::IsItemHovered()) {
2241 ImGui::SetTooltip(tr("Apply all enabled patches to the loaded ROM"));
2242 }
2243
2244 ImGui::SameLine();
2245 if (ImGui::Button(ICON_MD_SAVE " Save All")) {
2246 auto status = patch_manager_.SaveAllPatches();
2247 if (!status.ok()) {
2248 LOG_ERROR("Settings", "Failed to save patches: %s", status.message());
2249 }
2250 }
2251
2252 if (ImGui::Button(ICON_MD_REFRESH " Reload Patches")) {
2253 patches_loaded_ = false;
2254 selected_patch_ = nullptr;
2255 }
2256}
2257
2258void SettingsPanel::DrawPatchList(const std::string& folder) {
2259 auto patches = patch_manager_.GetPatchesInFolder(folder);
2260
2261 if (patches.empty()) {
2262 ImGui::TextDisabled(tr("No patches in this folder"));
2263 return;
2264 }
2265
2266 // Use a child region for scrolling
2267 float available_height = std::min(200.0f, patches.size() * 25.0f + 10.0f);
2268 if (ImGui::BeginChild("##PatchList", ImVec2(0, available_height), true)) {
2269 for (auto* patch : patches) {
2270 ImGui::PushID(patch->filename().c_str());
2271
2272 bool enabled = patch->enabled();
2273 if (ImGui::Checkbox("##Enabled", &enabled)) {
2274 patch->set_enabled(enabled);
2275 }
2276
2277 ImGui::SameLine();
2278
2279 // Highlight selected patch
2280 bool is_selected = (selected_patch_ == patch);
2281 if (ImGui::Selectable(patch->name().c_str(), is_selected)) {
2282 selected_patch_ = patch;
2283 }
2284
2285 ImGui::PopID();
2286 }
2287 }
2288 ImGui::EndChild();
2289}
2290
2292 if (!selected_patch_)
2293 return;
2294
2295 ImGui::Text("%s %s", ICON_MD_INFO, selected_patch_->name().c_str());
2296
2297 if (!selected_patch_->author().empty()) {
2298 ImGui::TextDisabled(tr("by %s"), selected_patch_->author().c_str());
2299 }
2300
2301 if (!selected_patch_->version().empty()) {
2302 ImGui::SameLine();
2303 ImGui::TextDisabled(tr("v%s"), selected_patch_->version().c_str());
2304 }
2305
2306 // Description
2307 if (!selected_patch_->description().empty()) {
2308 ImGui::Spacing();
2309 ImGui::TextWrapped("%s", selected_patch_->description().c_str());
2310 }
2311
2312 // Parameters
2313 auto& params = selected_patch_->mutable_parameters();
2314 if (!params.empty()) {
2315 ImGui::Spacing();
2316 ImGui::Text(tr("%s Parameters"), ICON_MD_TUNE);
2317 ImGui::Separator();
2318
2319 for (auto& param : params) {
2320 DrawParameterWidget(&param);
2321 }
2322 }
2323}
2324
2326 if (!param)
2327 return;
2328
2329 ImGui::PushID(param->define_name.c_str());
2330
2331 switch (param->type) {
2335 int value = param->value;
2336 const char* format = param->use_decimal ? "%d" : "$%X";
2337
2338 ImGui::Text("%s", param->display_name.c_str());
2339 ImGui::SetNextItemWidth(std::clamp(
2340 ImGui::GetContentRegionAvail().x * 0.50f,
2341 gui::ScaledSize(120.0f, 0.0f).x, gui::ScaledSize(260.0f, 0.0f).x));
2342 if (ImGui::InputInt("##Value", &value, 1, 16)) {
2343 param->value = std::clamp(value, param->min_value, param->max_value);
2344 }
2345
2346 // Show range hint
2347 if (param->min_value != 0 || param->max_value != 0xFF) {
2348 ImGui::SameLine();
2349 ImGui::TextDisabled("(%d-%d)", param->min_value, param->max_value);
2350 }
2351 break;
2352 }
2353
2355 bool checked = (param->value == param->checked_value);
2356 if (ImGui::Checkbox(param->display_name.c_str(), &checked)) {
2357 param->value = checked ? param->checked_value : param->unchecked_value;
2358 }
2359 break;
2360 }
2361
2363 ImGui::Text("%s", param->display_name.c_str());
2364 for (size_t i = 0; i < param->choices.size(); ++i) {
2365 bool selected = (param->value == static_cast<int>(i));
2366 if (ImGui::RadioButton(param->choices[i].c_str(), selected)) {
2367 param->value = static_cast<int>(i);
2368 }
2369 }
2370 break;
2371 }
2372
2374 ImGui::Text("%s", param->display_name.c_str());
2375 for (size_t i = 0; i < param->choices.size(); ++i) {
2376 if (param->choices[i].empty() || param->choices[i] == "_EMPTY") {
2377 continue;
2378 }
2379 bool bit_set = (param->value & (1 << i)) != 0;
2380 if (ImGui::Checkbox(param->choices[i].c_str(), &bit_set)) {
2381 if (bit_set) {
2382 param->value |= (1 << i);
2383 } else {
2384 param->value &= ~(1 << i);
2385 }
2386 }
2387 }
2388 break;
2389 }
2390
2392 ImGui::Text("%s", param->display_name.c_str());
2393 // TODO: Implement item dropdown using game item names
2394 ImGui::SetNextItemWidth(-1.0f);
2395 if (ImGui::InputInt(tr("Item ID"), &param->value)) {
2396 param->value = std::clamp(param->value, 0, 255);
2397 }
2398 break;
2399 }
2400 }
2401
2402 ImGui::PopID();
2403 ImGui::Spacing();
2404}
2405
2406} // namespace editor
2407} // namespace yaze
auto filename() const
Definition rom.h:219
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
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:122
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:257
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:81
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:97
std::vector< uint16_t > track_object_ids
Definition project.h:104
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:105
std::vector< uint16_t > track_stop_tiles
Definition project.h:100
std::vector< uint16_t > track_tiles
Definition project.h:99
std::vector< uint16_t > track_switch_tiles
Definition project.h:101
std::string expected_hash
Definition project.h:228
RomWritePolicy write_policy
Definition project.h:229
std::string rom_backup_folder
Definition project.h:309
std::string git_repository
Definition project.h:390
core::HackManifest hack_manifest
Definition project.h:376
std::string hack_manifest_file
Definition project.h:332
WorkspaceSettings workspace_settings
Definition project.h:362
std::string output_folder
Definition project.h:383
DungeonOverlaySettings dungeon_overlay
Definition project.h:363
std::string symbols_filename
Definition project.h:318
Z3dkSettings z3dk_settings
Definition project.h:379