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