yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_group_panel.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
3#include "util/i18n/tr.h"
4
5#include <algorithm>
6#include <cctype>
7#include <chrono>
8#include <string>
9#include <vector>
10
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
14#include "absl/strings/strip.h"
20#include "app/gui/core/color.h"
21#include "app/gui/core/icons.h"
24#include "imgui/imgui.h"
25#include "imgui/imgui_internal.h"
26#include "util/json.h"
27
28namespace yaze {
29namespace editor {
30
31using namespace yaze::gui;
37
38namespace {
39
40absl::StatusOr<uint16_t> ParseSnesHexToken(std::string token) {
41 token = std::string(absl::StripAsciiWhitespace(token));
42 if (token.empty()) {
43 return absl::InvalidArgumentError("Empty color token");
44 }
45
46 if (token[0] == '$') {
47 token.erase(0, 1);
48 } else if (token.size() > 2 &&
49 (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0)) {
50 token.erase(0, 2);
51 }
52
53 if (token.empty()) {
54 return absl::InvalidArgumentError("Color token is missing hex digits");
55 }
56
57 for (char ch : token) {
58 if (!std::isxdigit(static_cast<unsigned char>(ch))) {
59 return absl::InvalidArgumentError(
60 absl::StrCat("Invalid hex digit in color token: ", token));
61 }
62 }
63
64 if (token.size() > 4) {
65 return absl::InvalidArgumentError(
66 absl::StrCat("Color token is too long for SNES color: ", token));
67 }
68
69 uint32_t value = 0;
70 try {
71 value = static_cast<uint32_t>(std::stoul(token, nullptr, 16));
72 } catch (const std::exception&) {
73 return absl::InvalidArgumentError(
74 absl::StrCat("Failed to parse color token: ", token));
75 }
76
77 if (value > 0x7FFF) {
78 return absl::InvalidArgumentError(
79 absl::StrCat("SNES color out of range (0x0000-0x7FFF): ", token));
80 }
81
82 return static_cast<uint16_t>(value);
83}
84
85absl::StatusOr<std::vector<uint16_t>> ParseClipboardColors(
86 const std::string& clipboard) {
87 std::vector<uint16_t> colors;
88 for (const auto& raw_token : absl::StrSplit(
89 clipboard, absl::ByAnyChar(", \n\r\t"), absl::SkipEmpty())) {
90 const std::string token =
91 std::string(absl::StripAsciiWhitespace(raw_token));
92 if (token.empty()) {
93 continue;
94 }
95 auto color_or = ParseSnesHexToken(token);
96 if (!color_or.ok()) {
97 return color_or.status();
98 }
99 colors.push_back(*color_or);
100 }
101
102 if (colors.empty()) {
103 return absl::InvalidArgumentError("No colors found in clipboard data");
104 }
105
106 return colors;
107}
108
109#if defined(YAZE_WITH_JSON)
110absl::StatusOr<uint16_t> ParseSnesColorJson(const yaze::Json& value) {
111 if (value.is_string()) {
112 return ParseSnesHexToken(value.get<std::string>());
113 }
114 if (value.is_number_integer()) {
115 int parsed = value.get<int>();
116 if (parsed < 0 || parsed > 0x7FFF) {
117 return absl::InvalidArgumentError(
118 absl::StrFormat("SNES color out of range: %d", parsed));
119 }
120 return static_cast<uint16_t>(parsed);
121 }
122 if (value.is_number_unsigned()) {
123 uint32_t parsed = value.get<uint32_t>();
124 if (parsed > 0x7FFF) {
125 return absl::InvalidArgumentError(
126 absl::StrFormat("SNES color out of range: %u", parsed));
127 }
128 return static_cast<uint16_t>(parsed);
129 }
130 return absl::InvalidArgumentError(
131 "Invalid color value type (expected string or number)");
132}
133#endif
134
135} // namespace
136
137PaletteGroupPanel::PaletteGroupPanel(const std::string& group_name,
138 const std::string& display_name, Rom* rom,
139 zelda3::GameData* game_data)
140 : group_name_(group_name),
141 display_name_(display_name),
142 rom_(rom),
143 game_data_(game_data) {
144 // Note: We can't call GetPaletteGroup() here because it's a pure virtual
145 // function and the derived class isn't fully constructed yet. Original
146 // palettes will be loaded on first Draw() call instead.
147}
148
149void PaletteGroupPanel::Draw(bool* p_open) {
150 DrawContent(false);
151}
152
156
158 const auto* group = GetPaletteGroup();
160 group && group->size() > 0
161 ? std::clamp(index, 0, static_cast<int>(group->size()) - 1)
162 : 0;
163 selected_color_ = -1;
164}
165
167 if (!IsManagedSession() || !rom_ || !rom_->is_loaded())
168 return;
169 if (!embedded)
170 DrawToolbar();
172 bool batch_requested = false;
173 if (embedded) {
174 ImGui::SameLine();
175 if (ThemedIconButton(ICON_MD_MORE_HORIZ, "Selected palette tools"))
176 ImGui::OpenPopup("SelectedPaletteTools");
177 if (ImGui::BeginPopup("SelectedPaletteTools")) {
178 if (ImGui::MenuItem(tr("Copy colors")))
180 if (ImGui::MenuItem(tr("Paste colors"))) {
181 auto result = ImportFromClipboard();
182 if (!result.ok() && toast_manager_)
183 toast_manager_->Show(std::string(result.message()),
185 }
186 if (ImGui::MenuItem(tr("Batch operations...")))
187 batch_requested = true;
188 ImGui::EndPopup();
189 }
190 }
191 ImGui::Separator();
192 const bool columns =
193 ImGui::GetContentRegionAvail().x >= ImGui::GetFontSize() * 44;
194 const auto draw_grid = [this]() {
196 ImGui::Spacing();
197 if (ImGui::CollapsingHeader(tr("Details"))) {
200 }
201 };
202 const auto draw_picker = [this]() {
203 if (selected_color_ >= 0) {
205 if (ImGui::CollapsingHeader(tr("Color values")))
207 } else {
208 ImGui::TextDisabled(tr("Select a color to edit"));
209 }
210 };
211 if (columns &&
212 ImGui::BeginTable("PaletteContent", 2, ImGuiTableFlags_Resizable)) {
213 ImGui::TableSetupColumn("Colors", ImGuiTableColumnFlags_WidthStretch, 0.6f);
214 ImGui::TableSetupColumn("Edit", ImGuiTableColumnFlags_WidthStretch, 0.4f);
215 ImGui::TableNextColumn();
216 draw_grid();
217 ImGui::TableNextColumn();
218 draw_picker();
219 ImGui::EndTable();
220 } else if (!columns) {
221 draw_grid();
222 ImGui::Spacing();
223 draw_picker();
224 }
225 if (batch_requested)
226 ImGui::OpenPopup("BatchOperations");
228}
229
231 auto* palette = GetMutablePalette(selected_palette_);
232 if (!palette)
233 return;
234 const int columns = GetColorsPerRow();
235 const float spacing = ImGui::GetStyle().ItemSpacing.x;
236 const float cell = std::clamp(
237 (ImGui::GetContentRegionAvail().x - (columns - 1) * spacing) / columns,
238 12.0f, ImGui::GetFrameHeight() * 1.35f);
239 for (int i = 0; i < palette->size(); ++i) {
240 ImGui::PushID(i);
241 if (gui::PaletteColorButton("##color", (*palette)[i], i == selected_color_,
243 ImVec2(cell, cell)))
244 selected_color_ = i;
245 gui::AutoRegisterLastItem("button", absl::StrFormat("Palette color %d", i));
246 ImGui::PopID();
247 if ((i + 1) % columns && i + 1 < palette->size())
248 ImGui::SameLine();
249 }
250}
251
253 // Query PaletteManager for group-specific modification status
255
256 // Save button (primary action)
257 ImGui::BeginDisabled(!has_changes);
258 if (PrimaryButton(
259 absl::StrFormat("%s Apply to buffer", ICON_MD_SAVE).c_str())) {
260 auto status = SaveToRom();
261 if (!status.ok()) {
262 if (toast_manager_) {
263 toast_manager_->Show(absl::StrFormat("Failed to save %s: %s",
264 display_name_, status.message()),
266 }
267 }
268 }
269 ImGui::EndDisabled();
270 if (!has_changes &&
271 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
272 ImGui::SetTooltip(tr("No palette changes to apply"));
273 }
274
275 ImGui::SameLine();
276
277 // Discard button (danger action)
278 ImGui::BeginDisabled(!has_changes);
279 if (DangerButton(absl::StrFormat("%s Discard", ICON_MD_UNDO).c_str())) {
281 }
282 ImGui::EndDisabled();
283 if (!has_changes &&
284 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
285 ImGui::SetTooltip(tr("No palette changes to discard"));
286 }
287
288 ImGui::SameLine();
289
290 // Modified indicator badge (show modified color count for this group)
291 if (has_changes) {
292 size_t modified_count = 0;
293 auto* group = GetPaletteGroup();
294 if (group) {
295 for (int p = 0; p < group->size(); p++) {
297 modified_count++;
298 }
299 }
300 }
301 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), tr("%s %zu modified"),
302 ICON_MD_EDIT, modified_count);
303 }
304
305 ImGui::SameLine();
306 ImGui::Dummy(ImVec2(20, 0)); // Spacer
307 ImGui::SameLine();
308
309 // Undo/Redo (global operations via PaletteManager)
310 bool can_undo = gfx::PaletteManager::Get().CanUndo();
311 ImGui::BeginDisabled(!can_undo);
312 if (ThemedIconButton(ICON_MD_UNDO, "Undo")) {
313 Undo();
314 }
315 ImGui::EndDisabled();
316 if (!can_undo && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
317 ImGui::SetTooltip(tr("Nothing to undo"));
318 }
319
320 ImGui::SameLine();
321 bool can_redo = gfx::PaletteManager::Get().CanRedo();
322 ImGui::BeginDisabled(!can_redo);
323 if (ThemedIconButton(ICON_MD_REDO, "Redo")) {
324 Redo();
325 }
326 ImGui::EndDisabled();
327 if (!can_redo && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
328 ImGui::SetTooltip(tr("Nothing to redo"));
329 }
330
331 ImGui::SameLine();
332 ImGui::Dummy(ImVec2(20, 0)); // Spacer
333 ImGui::SameLine();
334
335 // Export/Import
336 if (ThemedIconButton(ICON_MD_FILE_DOWNLOAD, "Export to clipboard")) {
338 }
339
340 ImGui::SameLine();
341 if (ThemedIconButton(ICON_MD_FILE_UPLOAD, "Import from clipboard")) {
343 }
344
345 ImGui::SameLine();
346 if (ThemedIconButton(ICON_MD_MORE_VERT, "Batch operations")) {
347 ImGui::OpenPopup("BatchOperations");
348 }
349
350 // Custom toolbar buttons from derived classes
352}
353
355 auto* palette_group = GetPaletteGroup();
356 if (!palette_group)
357 return;
358
359 int num_palettes = palette_group->size();
360
361 ImGui::Text(tr("Palette:"));
362 ImGui::SameLine();
363
364 ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth());
365 if (ImGui::BeginCombo(
366 "##PaletteSelect",
367 absl::StrFormat("Palette %d", selected_palette_).c_str())) {
368 const float item_height = ImGui::GetTextLineHeightWithSpacing();
369 ImGuiListClipper clipper;
370 clipper.Begin(num_palettes, item_height);
371 if (selected_palette_ >= 0 && selected_palette_ < num_palettes) {
372 clipper.IncludeItemByIndex(selected_palette_);
373 }
374
375 while (clipper.Step()) {
376 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
377 bool is_selected = (selected_palette_ == i);
378 bool is_modified = IsPaletteModified(i);
379
380 std::string label = absl::StrFormat("Palette %d", i);
381 if (is_modified) {
382 label += " *";
383 }
384
385 if (ImGui::Selectable(label.c_str(), is_selected)) {
387 selected_color_ = -1; // Reset color selection
388 }
389 if (is_selected) {
390 ImGui::SetItemDefaultFocus();
391 }
392 }
393 }
394 ImGui::EndCombo();
395 }
396
397 // Show reset button for current palette
398 ImGui::SameLine();
399 ImGui::BeginDisabled(!IsPaletteModified(selected_palette_));
400 if (ThemedIconButton(ICON_MD_RESTORE, "Reset palette to original")) {
402 }
403 ImGui::EndDisabled();
404}
405
407 if (selected_color_ < 0)
408 return;
409
410 auto* palette = GetMutablePalette(selected_palette_);
411 if (!palette)
412 return;
413
414 SectionHeader("Color Editor");
415
416 if (selected_color_ >= palette->size())
417 return;
419
420 auto& manager = gfx::PaletteManager::Get();
421 const ImGuiID picker_id = ImGui::GetID("##picker");
422 if (active_picker_ == picker_id &&
423 ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
424 manager.CancelColorEdit(picker_id);
425 picker_cancelled_ = true;
426 }
427
428 // Read current model state so Undo/Redo and comparison windows stay in sync.
429 ImGui::SetNextItemWidth(
430 std::min(ImGui::GetContentRegionAvail().x, ImGui::GetFontSize() * 19));
431 ImVec4 col = ConvertSnesColorToImVec4((*palette)[selected_color_]);
432 ImGui::BeginDisabled(picker_cancelled_ && active_picker_ == picker_id);
433 const bool changed = ImGui::ColorPicker4(
434 "##picker", &col.x,
435 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel |
436 ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV);
437 const bool active = ImGui::IsItemActive();
438 // ColorPicker is a group with ID zero while idle; register its actual bounds
439 // with our stable ID so automation can discover it before starting a drag.
441 picker_metadata.label = "Palette color picker";
442 picker_metadata.window_name = ImGui::GetCurrentWindow()->Name;
443 picker_metadata.visible = ImGui::IsItemVisible();
444 picker_metadata.enabled = !picker_cancelled_;
445 const auto min = ImGui::GetItemRectMin();
446 const auto max = ImGui::GetItemRectMax();
447 picker_metadata.bounds = {min.x, min.y, max.x, max.y, true};
449 absl::StrCat("Palette/picker:", picker_id), "color", picker_id,
450 "Drag to preview; release commits one Undo step; Escape cancels",
451 picker_metadata);
452 ImGui::EndDisabled();
453 if (!picker_cancelled_) {
454 if (active &&
455 (active_picker_ != picker_id || !manager.HasColorEdit(picker_id))) {
456 const auto status = manager.BeginColorEdit(group_name_, selected_palette_,
457 selected_color_, picker_id);
458 if (status.ok()) {
459 active_picker_ = picker_id;
460 }
461 }
462 if (changed) {
465 }
466 if (!active && active_picker_ == picker_id) {
467 manager.CommitColorEdit(picker_id);
468 active_picker_ = 0;
469 }
470 } else if (active_picker_ == picker_id && !ImGui::IsMouseDown(0)) {
471 // Keep a held mouse from starting another edit immediately after Escape.
472 picker_cancelled_ = false;
473 active_picker_ = 0;
474 }
475 col = ConvertSnesColorToImVec4((*palette)[selected_color_]);
476
477 char snes[5];
478 absl::SNPrintF(snes, sizeof(snes), "%04X",
479 (*palette)[selected_color_].snes());
480 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 7);
481 if (ImGui::InputText("SNES", snes, sizeof(snes),
482 ImGuiInputTextFlags_CharsHexadecimal |
483 ImGuiInputTextFlags_EnterReturnsTrue)) {
484 const auto parsed = ParseSnesHexToken(snes);
485 if (parsed.ok()) {
487 } else if (toast_manager_) {
488 toast_manager_->Show(std::string(parsed.status().message()),
490 }
491 }
492 gui::AutoRegisterLastItem("input", "SNES color");
493
494 // Current vs Original comparison
495 ImGui::Separator();
496 ImGui::Text(tr("Current vs Original"));
497
498 ImGui::ColorButton("##current", col,
499 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker,
500 ImVec2(60, 40));
501
502 gui::AutoRegisterLastItem("color", "Current palette color");
503
504 ImGui::SameLine();
505
506 ImVec4 orig_col = ConvertSnesColorToImVec4(original);
507 if (ImGui::ColorButton(
508 "##original", orig_col,
509 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker,
510 ImVec2(60, 40))) {
511 // Click to restore original
513 }
514
515 if (ImGui::IsItemHovered()) {
516 ImGui::SetTooltip(tr("Click to restore original color"));
517 }
518
519 gui::AutoRegisterLastItem("button", "Restore original palette color");
520
521 // Reset button
522 ImGui::BeginDisabled(!IsColorModified(selected_palette_, selected_color_));
523 if (ThemedButton(absl::StrFormat("%s Reset", ICON_MD_RESTORE).c_str(),
524 ImVec2(-1, 0))) {
526 }
527 ImGui::EndDisabled();
528}
529
531 if (selected_color_ < 0)
532 return;
533
534 SectionHeader("Color Information");
535
536 const auto* palette = GetMutablePalette(selected_palette_);
537 if (!palette || selected_color_ >= palette->size())
538 return;
539 const auto color = (*palette)[selected_color_];
540 auto col = color.rgb();
541 int r = static_cast<int>(col.x);
542 int g = static_cast<int>(col.y);
543 int b = static_cast<int>(col.z);
544
545 // RGB values
546 ImGui::Text(tr("RGB (0-255): (%d, %d, %d)"), r, g, b);
547 if (ImGui::IsItemClicked()) {
548 ImGui::SetClipboardText(absl::StrFormat("(%d, %d, %d)", r, g, b).c_str());
549 }
550
551 // SNES BGR555 value
552 if (show_snes_format_) {
553 ImGui::Text(tr("SNES BGR555: $%04X"), color.snes());
554 if (ImGui::IsItemClicked()) {
555 ImGui::SetClipboardText(absl::StrFormat("$%04X", color.snes()).c_str());
556 }
557 }
558
559 // Hex value
560 if (show_hex_format_) {
561 ImGui::Text(tr("Hex: #%02X%02X%02X"), r, g, b);
562 if (ImGui::IsItemClicked()) {
563 ImGui::SetClipboardText(
564 absl::StrFormat("#%02X%02X%02X", r, g, b).c_str());
565 }
566 }
567
568 ImGui::TextDisabled(tr("Click any value to copy"));
569}
570
572 const auto& metadata = GetMetadata();
573 if (selected_palette_ >= metadata.palettes.size())
574 return;
575
576 const auto& pal_meta = metadata.palettes[selected_palette_];
577
578 SectionHeader("Palette Metadata");
579
580 // Palette ID
581 ImGui::Text(tr("Palette ID: %d"), pal_meta.palette_id);
582
583 // Name
584 if (!pal_meta.name.empty()) {
585 ImGui::Text(tr("Name: %s"), pal_meta.name.c_str());
586 }
587
588 // Description
589 if (!pal_meta.description.empty()) {
590 ImGui::TextWrapped("%s", pal_meta.description.c_str());
591 }
592
593 ImGui::Separator();
594
595 // Palette dimensions and color depth
596 ImGui::Text(tr("Dimensions: %d colors (%dx%d)"), metadata.colors_per_palette,
597 metadata.colors_per_row,
598 (metadata.colors_per_palette + metadata.colors_per_row - 1) /
599 metadata.colors_per_row);
600
601 ImGui::Text(tr("Color Depth: %d BPP (4-bit SNES)"), 4);
602 ImGui::TextDisabled(tr("(16 colors per palette possible)"));
603
604 ImGui::Separator();
605
606 // ROM Address
607 ImGui::Text(tr("ROM Address: $%06X"), pal_meta.rom_address);
608 if (ImGui::IsItemClicked()) {
609 ImGui::SetClipboardText(
610 absl::StrFormat("$%06X", pal_meta.rom_address).c_str());
611 }
612 if (ImGui::IsItemHovered()) {
613 ImGui::SetTooltip(tr("Click to copy address"));
614 }
615
616 // VRAM Address (if applicable)
617 if (pal_meta.vram_address > 0) {
618 ImGui::Text(tr("VRAM Address: $%04X"), pal_meta.vram_address);
619 if (ImGui::IsItemClicked()) {
620 ImGui::SetClipboardText(
621 absl::StrFormat("$%04X", pal_meta.vram_address).c_str());
622 }
623 if (ImGui::IsItemHovered()) {
624 ImGui::SetTooltip(tr("Click to copy VRAM address"));
625 }
626 }
627
628 // Usage notes
629 if (!pal_meta.usage_notes.empty()) {
630 ImGui::Separator();
631 ImGui::TextDisabled(tr("Usage Notes:"));
632 ImGui::TextWrapped("%s", pal_meta.usage_notes.c_str());
633 }
634}
635
637 if (ImGui::BeginPopup("BatchOperations")) {
638 SectionHeader("Batch Operations");
639
640 if (ThemedButton("Copy Current Palette", ImVec2(-1, 0))) {
642 ImGui::CloseCurrentPopup();
643 }
644
645 if (ThemedButton("Paste to Current Palette", ImVec2(-1, 0))) {
647 ImGui::CloseCurrentPopup();
648 }
649
650 ImGui::Separator();
651
652 if (ThemedButton("Reset All Palettes", ImVec2(-1, 0))) {
654 ImGui::CloseCurrentPopup();
655 }
656
657 ImGui::EndPopup();
658 }
659}
660
661// ========== Palette Operations ==========
662
663void PaletteGroupPanel::SetColor(int palette_index, int color_index,
664 const gfx::SnesColor& new_color) {
665 if (!IsManagedSession()) {
666 if (toast_manager_) {
667 toast_manager_->Show("Cannot edit palettes from an inactive ROM session",
669 }
670 return;
671 }
672
673 // Delegate to PaletteManager for centralized tracking and undo/redo
674 auto status = gfx::PaletteManager::Get().SetColor(group_name_, palette_index,
675 color_index, new_color);
676 if (!status.ok()) {
677 if (toast_manager_) {
679 absl::StrFormat("Failed to set color: %s", status.message()),
681 }
682 return;
683 }
684}
685
687 if (!IsManagedSession()) {
688 return absl::FailedPreconditionError(
689 "Cannot save palettes from an inactive ROM session");
690 }
691 auto& palette_manager = gfx::PaletteManager::Get();
692 const auto ranges =
693 palette_manager.GetModifiedGroupColorWriteRanges(group_name_);
694 if (!ranges.ok()) {
695 return ranges.status();
696 }
697 if (project_ != nullptr && project_->hack_manifest.loaded()) {
698 if (!ranges->empty()) {
699 const absl::Status preflight = ValidateHackManifestSaveConflicts(
701 display_name_, "PaletteGroupPanel", toast_manager_);
702 if (!preflight.ok()) {
703 return preflight;
704 }
705 }
706 }
707
708 // Delegate to PaletteManager for centralized save operation.
709 return palette_manager.SaveGroup(group_name_);
710}
711
713 if (!IsManagedSession()) {
714 return;
715 }
716 // Delegate to PaletteManager for centralized discard operation
718
719 // Reset selection
720 selected_color_ = -1;
721}
722
723void PaletteGroupPanel::ResetPalette(int palette_index) {
724 if (!IsManagedSession()) {
725 return;
726 }
727 // Delegate to PaletteManager for centralized reset operation
729}
730
731void PaletteGroupPanel::ResetColor(int palette_index, int color_index) {
732 if (!IsManagedSession()) {
733 return;
734 }
735 // Delegate to PaletteManager for centralized reset operation
737 color_index);
738}
739
740// ========== History Management ==========
741
743 if (!IsManagedSession()) {
744 return;
745 }
746 // Delegate to PaletteManager's global undo system
748}
749
751 if (!IsManagedSession()) {
752 return;
753 }
754 // Delegate to PaletteManager's global redo system
756}
757
759 if (!IsManagedSession()) {
760 return;
761 }
762 // Delegate to PaletteManager's global history
764}
765
766// ========== State Queries ==========
767
768bool PaletteGroupPanel::IsPaletteModified(int palette_index) const {
769 if (!IsManagedSession()) {
770 return false;
771 }
772 // Query PaletteManager for modification status
774 palette_index);
775}
776
778 int color_index) const {
779 if (!IsManagedSession()) {
780 return false;
781 }
782 // Query PaletteManager for modification status
784 color_index);
785}
786
788 if (!IsManagedSession()) {
789 return false;
790 }
791 // Query PaletteManager for group-specific modification status
793}
794
796 if (!IsManagedSession()) {
797 return false;
798 }
799 // Query PaletteManager for global undo availability
801}
802
804 if (!IsManagedSession()) {
805 return false;
806 }
807 // Query PaletteManager for global redo availability
809}
810
811// ========== Helper Methods ==========
812
814 auto* palette_group = GetPaletteGroup();
815 if (!palette_group || index < 0 || index >= palette_group->size()) {
816 return nullptr;
817 }
818 return palette_group->mutable_palette(index);
819}
820
822 int color_index) const {
823 // Get original color from PaletteManager's snapshots
825 color_index);
826}
827
832
833// MarkModified and ClearModified removed - PaletteManager handles tracking now
834
835// ========== Export/Import ==========
836
838#if defined(YAZE_WITH_JSON)
839 auto* palette_group = GetPaletteGroup();
840 if (!palette_group) {
841 return "{}";
842 }
843
845 root["version"] = 1;
846 root["group"] = group_name_;
847 root["display_name"] = display_name_;
848 root["palettes"] = yaze::Json::array();
849
850 for (size_t palette_index = 0; palette_index < palette_group->size();
851 palette_index++) {
852 const auto& palette =
853 palette_group->palette_ref(static_cast<int>(palette_index));
854 yaze::Json palette_json = yaze::Json::object();
855 palette_json["index"] = static_cast<int>(palette_index);
856 palette_json["colors"] = yaze::Json::array();
857
858 for (size_t color_index = 0; color_index < palette.size(); color_index++) {
859 palette_json["colors"].push_back(
860 absl::StrFormat("$%04X", palette[color_index].snes()));
861 }
862
863 root["palettes"].push_back(palette_json);
864 }
865
866 return root.dump(2);
867#else
868 return "{}";
869#endif
870}
871
872absl::Status PaletteGroupPanel::ImportFromJson(const std::string& json) {
873 if (!IsManagedSession()) {
874 return absl::FailedPreconditionError(
875 "Cannot import palettes into an inactive ROM session");
876 }
877#if !defined(YAZE_WITH_JSON)
878 return absl::UnimplementedError("JSON support is disabled");
879#else
880 auto* palette_group = GetPaletteGroup();
881 if (!palette_group) {
882 return absl::FailedPreconditionError("Palette group is unavailable");
883 }
884
885 yaze::Json root;
886 try {
887 root = yaze::Json::parse(json);
888 } catch (const std::exception& e) {
889 return absl::InvalidArgumentError(
890 absl::StrCat("Failed to parse palette JSON: ", e.what()));
891 }
892
893 if (!root.is_object()) {
894 return absl::InvalidArgumentError("Palette JSON must be an object");
895 }
896
897 if (root.contains("version")) {
898 const auto& version_value = root["version"];
899 if (!version_value.is_number_integer() &&
900 !version_value.is_number_unsigned()) {
901 return absl::InvalidArgumentError(
902 "Palette JSON 'version' must be an integer");
903 }
904 int version = version_value.get<int>();
905 if (version != 1) {
906 return absl::InvalidArgumentError(
907 absl::StrFormat("Unsupported palette JSON version: %d", version));
908 }
909 }
910
911 if (root.contains("group")) {
912 const auto& group_value = root["group"];
913 if (!group_value.is_string()) {
914 return absl::InvalidArgumentError(
915 "Palette JSON 'group' must be a string");
916 }
917 const std::string group = group_value.get<std::string>();
918 if (group != group_name_) {
919 return absl::InvalidArgumentError(absl::StrFormat(
920 "Palette JSON group '%s' does not match '%s'", group, group_name_));
921 }
922 }
923
924 if (!root.contains("palettes") || !root["palettes"].is_array()) {
925 return absl::InvalidArgumentError(
926 "Palette JSON must contain a 'palettes' array");
927 }
928
929 struct PaletteImport {
930 int index;
931 std::vector<uint16_t> colors;
932 };
933
934 std::vector<PaletteImport> imports;
935 for (const auto& palette_json : root["palettes"]) {
936 if (!palette_json.is_object()) {
937 return absl::InvalidArgumentError("Palette entry must be a JSON object");
938 }
939
940 if (!palette_json.contains("index") ||
941 !palette_json["index"].is_number_integer()) {
942 return absl::InvalidArgumentError(
943 "Palette entry is missing integer 'index'");
944 }
945
946 int palette_index = palette_json["index"].get<int>();
947 if (palette_index < 0 || palette_index >= palette_group->size()) {
948 return absl::InvalidArgumentError(absl::StrFormat(
949 "Palette index %d out of range [0, %d)", palette_index,
950 static_cast<int>(palette_group->size())));
951 }
952
953 if (!palette_json.contains("colors") ||
954 !palette_json["colors"].is_array()) {
955 return absl::InvalidArgumentError(
956 "Palette entry is missing 'colors' array");
957 }
958
959 std::vector<uint16_t> colors;
960 colors.reserve(palette_json["colors"].size());
961 for (const auto& color_json : palette_json["colors"]) {
962 auto color_or = ParseSnesColorJson(color_json);
963 if (!color_or.ok()) {
964 return color_or.status();
965 }
966 colors.push_back(*color_or);
967 }
968
969 const auto& palette = palette_group->palette_ref(palette_index);
970 if (colors.size() != palette.size()) {
971 return absl::InvalidArgumentError(absl::StrFormat(
972 "Palette %d expects %d colors but received %d", palette_index,
973 static_cast<int>(palette.size()), static_cast<int>(colors.size())));
974 }
975
976 imports.push_back({palette_index, std::move(colors)});
977 }
978
979 auto& manager = gfx::PaletteManager::Get();
980 manager.BeginBatch();
981 for (const auto& import : imports) {
982 for (size_t color_index = 0; color_index < import.colors.size();
983 color_index++) {
984 auto status = manager.SetColor(
985 group_name_, import.index, static_cast<int>(color_index),
986 gfx::SnesColor(import.colors[color_index]));
987 if (!status.ok()) {
988 manager.EndBatch();
989 return status;
990 }
991 }
992 }
993 manager.EndBatch();
994
995 return absl::OkStatus();
996#endif
997}
998
1000 auto* palette_group = GetPaletteGroup();
1001 if (!palette_group || selected_palette_ >= palette_group->size()) {
1002 return "";
1003 }
1004
1005 auto palette = palette_group->palette(selected_palette_);
1006 std::string result;
1007
1008 for (size_t i = 0; i < palette.size(); i++) {
1009 result += absl::StrFormat("$%04X", palette[i].snes());
1010 if (i < palette.size() - 1) {
1011 result += ",";
1012 }
1013 }
1014
1015 ImGui::SetClipboardText(result.c_str());
1016 return result;
1017}
1018
1020 if (!IsManagedSession()) {
1021 return absl::FailedPreconditionError(
1022 "Cannot import palettes into an inactive ROM session");
1023 }
1024 auto* palette = GetMutablePalette(selected_palette_);
1025 if (!palette) {
1026 return absl::FailedPreconditionError("No palette selected");
1027 }
1028
1029 const char* clipboard = ImGui::GetClipboardText();
1030 if (!clipboard || clipboard[0] == '\0') {
1031 return absl::InvalidArgumentError("Clipboard is empty");
1032 }
1033
1034 auto colors_or = ParseClipboardColors(clipboard);
1035 if (!colors_or.ok()) {
1036 return colors_or.status();
1037 }
1038
1039 const auto& colors = *colors_or;
1040 if (colors.size() != palette->size()) {
1041 return absl::InvalidArgumentError(absl::StrFormat(
1042 "Clipboard contains %d colors but palette expects %d",
1043 static_cast<int>(colors.size()), static_cast<int>(palette->size())));
1044 }
1045
1046 auto& manager = gfx::PaletteManager::Get();
1047 manager.BeginBatch();
1048 for (size_t color_index = 0; color_index < colors.size(); color_index++) {
1049 auto status = manager.SetColor(group_name_, selected_palette_,
1050 static_cast<int>(color_index),
1051 gfx::SnesColor(colors[color_index]));
1052 if (!status.ok()) {
1053 manager.EndBatch();
1054 return status;
1055 }
1056 }
1057 manager.EndBatch();
1058
1059 return absl::OkStatus();
1060}
1061
1062// ============================================================================
1063// Concrete Palette Panel Implementations
1064// ============================================================================
1065
1066// ========== Overworld Main Palette Panel ==========
1067
1070
1072 Rom* rom, zelda3::GameData* game_data)
1073 : PaletteGroupPanel("ow_main", "Overworld Main Palettes", rom, game_data) {}
1074
1076 PaletteGroupMetadata metadata;
1077 metadata.group_name = "ow_main";
1078 metadata.display_name = "Overworld Main Palettes";
1079 metadata.colors_per_palette = 35;
1080 metadata.colors_per_row = 7;
1081
1082 // ALTTP OW main palettes are 35 colors per set (5 sub-palettes x 7 colors),
1083 // stored contiguously in ROM and loaded into CGRAM starting at $0042.
1084 // See usdasm: bank_1B.asm -> PaletteLoad_OWBGMain / PaletteData_owmain_00.
1085 for (int i = 0; i < 6; i++) {
1086 PaletteMetadata pal;
1087 pal.palette_id = i;
1088 pal.name = absl::StrFormat("Overworld Main %02d", i);
1089 pal.description =
1090 "BG main palette set (35 colors = 5x7, transparent slots are implicit)";
1091 pal.rom_address = gfx::kOverworldPaletteMain + (i * (35 * 2));
1092 pal.vram_address = 0;
1093 pal.usage_notes =
1094 "Loaded by PaletteLoad_OWBGMain to CGRAM $0042 (rows 2-6, cols 1-7).";
1095 metadata.palettes.push_back(pal);
1096 }
1097
1098 return metadata;
1099}
1100
1102 if (!game_data_)
1103 return nullptr;
1104 return game_data_->palette_groups.get_group("ow_main");
1105}
1106
1108 if (!game_data_)
1109 return nullptr;
1110 return const_cast<zelda3::GameData*>(game_data_)
1111 ->palette_groups.get_group("ow_main");
1112}
1113
1114// ========== Overworld Animated Palette Panel ==========
1115
1118
1120 Rom* rom, zelda3::GameData* game_data)
1121 : PaletteGroupPanel("ow_animated", "Overworld Animated Palettes", rom,
1122 game_data) {}
1123
1125 PaletteGroupMetadata metadata;
1126 metadata.group_name = "ow_animated";
1127 metadata.display_name = "Overworld Animated Palettes";
1128 metadata.colors_per_palette = 7;
1129 metadata.colors_per_row = 7;
1130
1131 // ALTTP OW animated palettes are 7 colors each, stored at kOverworldPaletteAnimated.
1132 // See usdasm: bank_1B.asm -> PaletteLoad_OWBG3 / PaletteData_owanim_00.
1133 for (int i = 0; i < 14; i++) {
1134 PaletteMetadata pal;
1135 pal.palette_id = i;
1136 pal.name = absl::StrFormat("OW Anim %02d", i);
1137 pal.description =
1138 "Animated overlay palette (7 colors, transparent slot is implicit)";
1139 pal.rom_address = gfx::kOverworldPaletteAnimated + (i * (7 * 2));
1140 pal.vram_address = 0;
1141 pal.usage_notes =
1142 "Loaded by PaletteLoad_OWBG3 to CGRAM $00E2 (row 7, cols 1-7).";
1143 metadata.palettes.push_back(pal);
1144 }
1145
1146 return metadata;
1147}
1148
1150 if (!game_data_)
1151 return nullptr;
1152 return game_data_->palette_groups.get_group("ow_animated");
1153}
1154
1156 const {
1157 if (!game_data_)
1158 return nullptr;
1159 return const_cast<zelda3::GameData*>(game_data_)
1160 ->palette_groups.get_group("ow_animated");
1161}
1162
1163// ========== Dungeon Main Palette Panel ==========
1164
1167
1169 zelda3::GameData* game_data)
1170 : PaletteGroupPanel("dungeon_main", "Dungeon Main Palettes", rom,
1171 game_data) {}
1172
1174 PaletteGroupMetadata metadata;
1175 metadata.group_name = "dungeon_main";
1176 metadata.display_name = "Dungeon Main Palettes";
1177 metadata.colors_per_palette = 90;
1178 metadata.colors_per_row = 15;
1179
1180 // Dungeon palettes (0-19)
1181 const char* dungeon_names[] = {
1182 "Sewers", "Hyrule Castle", "Eastern Palace", "Desert Palace",
1183 "Agahnim's Tower", "Swamp Palace", "Palace of Darkness", "Misery Mire",
1184 "Skull Woods", "Ice Palace", "Tower of Hera", "Thieves' Town",
1185 "Turtle Rock", "Ganon's Tower", "Generic 1", "Generic 2",
1186 "Generic 3", "Generic 4", "Generic 5", "Generic 6"};
1187
1188 for (int i = 0; i < 20; i++) {
1189 PaletteMetadata pal;
1190 pal.palette_id = i;
1191 pal.name = dungeon_names[i];
1192 pal.description = absl::StrFormat("Dungeon palette %d", i);
1193 pal.rom_address = gfx::kDungeonMainPalettes + (i * (90 * 2));
1194 pal.vram_address = 0;
1195 pal.usage_notes =
1196 "90 colors = 6 CGRAM banks x 15 colors (transparent slot per bank is "
1197 "implicit).";
1198 metadata.palettes.push_back(pal);
1199 }
1200
1201 return metadata;
1202}
1203
1205 if (!game_data_)
1206 return nullptr;
1207 return game_data_->palette_groups.get_group("dungeon_main");
1208}
1209
1211 if (!game_data_)
1212 return nullptr;
1213 return const_cast<zelda3::GameData*>(game_data_)
1214 ->palette_groups.get_group("dungeon_main");
1215}
1216
1217// ========== Sprite Palette Panel ==========
1218
1221
1223 : PaletteGroupPanel("global_sprites", "Sprite Palettes", rom, game_data) {}
1224
1226 PaletteGroupMetadata metadata;
1227 metadata.group_name = "global_sprites";
1228 metadata.display_name = "Global Sprite Palettes";
1229 metadata.colors_per_palette =
1230 60; // 4 sprite banks x 15 colors (transparent is implicit)
1231 metadata.colors_per_row = 15; // Display as 4 rows of 15 to match ROM layout
1232
1233 // 2 palette sets: Light World and Dark World
1234 const char* sprite_names[] = {"Global Sprites (Light World)",
1235 "Global Sprites (Dark World)"};
1236
1237 for (int i = 0; i < 2; i++) {
1238 PaletteMetadata pal;
1239 pal.palette_id = i;
1240 pal.name = sprite_names[i];
1241 pal.description =
1242 "60 colors = 4 sprite banks x 15 colors (transparent slots are "
1243 "implicit)";
1244 pal.rom_address =
1246 pal.vram_address = 0; // Palettes reside in PPU CGRAM (not VRAM)
1247 pal.usage_notes =
1248 "Loaded into CGRAM rows 9-12, cols 1-15 (row col0 is transparent).";
1249 metadata.palettes.push_back(pal);
1250 }
1251
1252 return metadata;
1253}
1254
1256 if (!game_data_)
1257 return nullptr;
1258 return game_data_->palette_groups.get_group("global_sprites");
1259}
1260
1262 if (!game_data_)
1263 return nullptr;
1264 return const_cast<zelda3::GameData*>(game_data_)
1265 ->palette_groups.get_group("global_sprites");
1266}
1267
1269 SectionHeader("CGRAM Placement");
1270 ImGui::TextWrapped(
1271 tr("Global sprite palettes are stored in ROM as 4 banks of 15 colors "
1272 "(transparent is implicit) and loaded to PPU CGRAM rows 9-12, cols "
1273 "1-15."));
1274 ImGui::TextDisabled(tr("Note: Palettes live in CGRAM, not VRAM."));
1275}
1276
1277// ========== Equipment Palette Panel ==========
1278
1281
1283 zelda3::GameData* game_data)
1284 : PaletteGroupPanel("armors", "Equipment Palettes", rom, game_data) {}
1285
1287 PaletteGroupMetadata metadata;
1288 metadata.group_name = "armors";
1289 metadata.display_name = "Equipment Palettes";
1290 metadata.colors_per_palette = 15;
1291 metadata.colors_per_row = 15;
1292
1293 const char* armor_names[] = {"Green Mail", "Blue Mail", "Red Mail", "Bunny",
1294 "Electrocuted"};
1295
1296 for (int i = 0; i < 5; i++) {
1297 PaletteMetadata pal;
1298 pal.palette_id = i;
1299 pal.name = armor_names[i];
1300 pal.description = absl::StrFormat("Link appearance: %s", armor_names[i]);
1301 pal.rom_address = gfx::kArmorPalettes + (i * (15 * 2));
1302 pal.vram_address = 0;
1303 pal.usage_notes =
1304 "15 colors per set (transparent slot is implicit when loaded into "
1305 "CGRAM).";
1306 metadata.palettes.push_back(pal);
1307 }
1308
1309 return metadata;
1310}
1311
1313 if (!game_data_)
1314 return nullptr;
1315 return game_data_->palette_groups.get_group("armors");
1316}
1317
1319 if (!game_data_)
1320 return nullptr;
1321 return const_cast<zelda3::GameData*>(game_data_)
1322 ->palette_groups.get_group("armors");
1323}
1324
1325// ========== Sprites Aux1 Palette Panel ==========
1326
1329
1331 zelda3::GameData* game_data)
1332 : PaletteGroupPanel("sprites_aux1", "Sprites Aux 1", rom, game_data) {}
1333
1335 PaletteGroupMetadata metadata;
1336 metadata.group_name = "sprites_aux1";
1337 metadata.display_name = "Sprites Aux 1";
1338 metadata.colors_per_palette = 7;
1339 metadata.colors_per_row = 7;
1340
1341 for (int i = 0; i < 12; i++) {
1342 PaletteMetadata pal;
1343 pal.palette_id = i;
1344 pal.name = absl::StrFormat("Sprites Aux1 %02d", i);
1345 pal.description =
1346 "Auxiliary sprite palette (7 colors, transparent is implicit)";
1347 pal.rom_address = 0xDD39E + (i * 14); // 7 colors * 2 bytes
1348 pal.vram_address = 0;
1349 pal.usage_notes =
1350 "Loaded into CGRAM with an implicit transparent slot at index 0 of the "
1351 "bank.";
1352 metadata.palettes.push_back(pal);
1353 }
1354
1355 return metadata;
1356}
1357
1359 if (!game_data_)
1360 return nullptr;
1361 return game_data_->palette_groups.get_group("sprites_aux1");
1362}
1363
1365 if (!game_data_)
1366 return nullptr;
1367 return const_cast<zelda3::GameData*>(game_data_)
1368 ->palette_groups.get_group("sprites_aux1");
1369}
1370
1371// ========== Sprites Aux2 Palette Panel ==========
1372
1375
1377 zelda3::GameData* game_data)
1378 : PaletteGroupPanel("sprites_aux2", "Sprites Aux 2", rom, game_data) {}
1379
1381 PaletteGroupMetadata metadata;
1382 metadata.group_name = "sprites_aux2";
1383 metadata.display_name = "Sprites Aux 2";
1384 metadata.colors_per_palette = 7;
1385 metadata.colors_per_row = 7;
1386
1387 for (int i = 0; i < 11; i++) {
1388 PaletteMetadata pal;
1389 pal.palette_id = i;
1390 pal.name = absl::StrFormat("Sprites Aux2 %02d", i);
1391 pal.description =
1392 "Auxiliary sprite palette (7 colors, transparent is implicit)";
1393 pal.rom_address = 0xDD446 + (i * 14); // 7 colors * 2 bytes
1394 pal.vram_address = 0;
1395 pal.usage_notes =
1396 "Loaded into CGRAM with an implicit transparent slot at index 0 of the "
1397 "bank.";
1398 metadata.palettes.push_back(pal);
1399 }
1400
1401 return metadata;
1402}
1403
1405 if (!game_data_)
1406 return nullptr;
1407 return game_data_->palette_groups.get_group("sprites_aux2");
1408}
1409
1411 if (!game_data_)
1412 return nullptr;
1413 return const_cast<zelda3::GameData*>(game_data_)
1414 ->palette_groups.get_group("sprites_aux2");
1415}
1416
1417// ========== Sprites Aux3 Palette Panel ==========
1418
1421
1423 zelda3::GameData* game_data)
1424 : PaletteGroupPanel("sprites_aux3", "Sprites Aux 3", rom, game_data) {}
1425
1427 PaletteGroupMetadata metadata;
1428 metadata.group_name = "sprites_aux3";
1429 metadata.display_name = "Sprites Aux 3";
1430 metadata.colors_per_palette = 7;
1431 metadata.colors_per_row = 7;
1432
1433 for (int i = 0; i < 24; i++) {
1434 PaletteMetadata pal;
1435 pal.palette_id = i;
1436 pal.name = absl::StrFormat("Sprites Aux3 %02d", i);
1437 pal.description =
1438 "Auxiliary sprite palette (7 colors, transparent is implicit)";
1439 pal.rom_address = 0xDD4E0 + (i * 14); // 7 colors * 2 bytes
1440 pal.vram_address = 0;
1441 pal.usage_notes =
1442 "Loaded into CGRAM with an implicit transparent slot at index 0 of the "
1443 "bank.";
1444 metadata.palettes.push_back(pal);
1445 }
1446
1447 return metadata;
1448}
1449
1451 if (!game_data_)
1452 return nullptr;
1453 return game_data_->palette_groups.get_group("sprites_aux3");
1454}
1455
1457 if (!game_data_)
1458 return nullptr;
1459 return const_cast<zelda3::GameData*>(game_data_)
1460 ->palette_groups.get_group("sprites_aux3");
1461}
1462
1463} // namespace editor
1464} // namespace yaze
bool is_object() const
Definition json.h:57
static Json parse(const std::string &)
Definition json.h:36
bool is_array() const
Definition json.h:58
static Json object()
Definition json.h:34
bool is_string() const
Definition json.h:59
static Json array()
Definition json.h:35
T get() const
Definition json.h:49
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
bool contains(const std::string &) const
Definition json.h:53
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
bool is_loaded() const
Definition rom.h:199
bool loaded() const
Check if the manifest has been loaded.
static const PaletteGroupMetadata metadata_
DungeonMainPalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
static PaletteGroupMetadata InitializeMetadata()
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
EquipmentPalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static PaletteGroupMetadata InitializeMetadata()
static const PaletteGroupMetadata metadata_
OverworldAnimatedPalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static const PaletteGroupMetadata metadata_
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
static PaletteGroupMetadata InitializeMetadata()
OverworldMainPalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static const PaletteGroupMetadata metadata_
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
Base class for palette group editing cards.
virtual gfx::PaletteGroup * GetPaletteGroup()=0
Get the palette group for this card.
void ResetPalette(int palette_index)
Reset a specific palette to original ROM values.
virtual void DrawCustomToolbarButtons()
Draw additional toolbar buttons (called after standard buttons)
void DrawPaletteSelector()
Draw palette selector dropdown.
void ResetColor(int palette_index, int color_index)
Reset a specific color to original ROM value.
gfx::SnesColor GetOriginalColor(int palette_index, int color_index) const
Get original color from ROM (for reset/comparison)
gfx::SnesPalette * GetMutablePalette(int index)
Get mutable palette by index.
void DrawPaletteGrid()
Draw the palette grid specific to this palette type.
void DiscardChanges()
Discard all unsaved changes.
void DrawToolbar()
Draw standard toolbar with save/discard/undo/redo.
void DrawBatchOperationsPopup()
Draw batch operations popup.
const project::YazeProject * project_
void DrawMetadataInfo()
Draw palette metadata info panel.
void SetColor(int palette_index, int color_index, const gfx::SnesColor &new_color)
Set a color value (records change for undo)
virtual const PaletteGroupMetadata & GetMetadata() const =0
Get metadata for this palette group.
absl::Status SaveToRom()
Save all modified palettes to ROM.
virtual void DrawCustomPanels()
Draw additional panels (called after main content)
void DrawColorPicker()
Draw color picker for selected color.
bool IsPaletteModified(int palette_index) const
PaletteGroupPanel(const std::string &group_name, const std::string &display_name, Rom *rom, zelda3::GameData *game_data=nullptr)
Construct a new Palette Group Panel.
bool IsColorModified(int palette_index, int color_index) const
bool IsManagedSession() const
Return true only while PaletteManager is bound to this panel's session.
void Draw(bool *p_open) override
Draw the card's ImGui UI.
void DrawColorInfo()
Draw color info panel with RGB/SNES/Hex values.
absl::Status ImportFromJson(const std::string &json)
virtual int GetColorsPerRow() const =0
Get the number of colors per row for grid layout.
static PaletteGroupMetadata InitializeMetadata()
static const PaletteGroupMetadata metadata_
SpritePalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
void DrawCustomPanels() override
Draw additional panels (called after main content)
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
static PaletteGroupMetadata InitializeMetadata()
SpritesAux1PalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static const PaletteGroupMetadata metadata_
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
static const PaletteGroupMetadata metadata_
static PaletteGroupMetadata InitializeMetadata()
SpritesAux2PalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static PaletteGroupMetadata InitializeMetadata()
SpritesAux3PalettePanel(Rom *rom, zelda3::GameData *game_data=nullptr)
static const PaletteGroupMetadata metadata_
gfx::PaletteGroup * GetPaletteGroup() override
Get the palette group for this card.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
absl::Status SetColor(const std::string &group_name, int palette_index, int color_index, const SnesColor &new_color)
Set a color in a palette (records change for undo)
bool IsGroupModified(const std::string &group_name) const
Check if a specific palette group has modifications.
void Undo()
Undo the most recent change.
void ClearHistory()
Clear undo/redo history.
bool IsManaging(const zelda3::GameData *game_data) const
Check whether the manager is bound to this exact GameData and ROM.
bool CanRedo() const
Check if the complete redo entry matches current colors. Conflicting history is retained,...
bool CanUndo() const
Check if the complete undo entry matches current colors. Conflicting history is retained,...
absl::Status ResetColor(const std::string &group_name, int palette_index, int color_index)
Reset a single color to its original ROM value.
absl::Status ResetPalette(const std::string &group_name, int palette_index)
Reset an entire palette to original ROM values.
SnesColor GetOriginalColor(const std::string &group_name, int palette_index, int color_index) const
bool IsColorModified(const std::string &group_name, int palette_index, int color_index) const
Check if a specific color is modified.
void DiscardGroup(const std::string &group_name)
Discard changes for a specific group.
static PaletteManager & Get()
Get the singleton instance.
bool IsPaletteModified(const std::string &group_name, int palette_index) const
Check if a specific palette is modified.
void Redo()
Redo the most recently undone change.
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
static float GetStandardInputWidth()
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()
#define ICON_MD_MORE_VERT
Definition icons.h:1243
#define ICON_MD_FILE_DOWNLOAD
Definition icons.h:744
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_RESTORE
Definition icons.h:1605
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_UNDO
Definition icons.h:2039
absl::StatusOr< uint16_t > ParseSnesHexToken(std::string token)
absl::StatusOr< std::vector< uint16_t > > ParseClipboardColors(const std::string &clipboard)
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
constexpr int kArmorPalettes
constexpr int kOverworldPaletteAnimated
constexpr int kOverworldPaletteMain
constexpr int kGlobalSpritesLW
constexpr int kGlobalSpritePalettesDW
constexpr int kDungeonMainPalettes
Graphical User Interface (GUI) components for the application.
bool ThemedIconButton(const char *icon, const char *tooltip, const ImVec2 &size, bool is_active, bool is_disabled, const char *panel_id, const char *anim_id)
Draw a standard icon button with theme-aware colors.
bool PrimaryButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a primary action button (accented color).
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
bool DangerButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a danger action button (error color).
void SectionHeader(const char *icon, const char *label, const ImVec4 &color)
IMGUI_API bool PaletteColorButton(const char *id, const gfx::SnesColor &color, bool is_selected, bool is_modified, const ImVec2 &size, ImGuiColorEditFlags flags)
Definition color.cc:463
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
Definition color.cc:24
gfx::SnesColor ConvertImVec4ToSnesColor(const ImVec4 &color)
Convert standard ImVec4 to SnesColor.
Definition color.cc:37
Metadata for an entire palette group.
std::vector< PaletteMetadata > palettes
Metadata for a single palette in a group.
PaletteGroup * get_group(const std::string &group_name)
Represents a group of palettes.
RomWritePolicy write_policy
Definition project.h:229
core::HackManifest hack_manifest
Definition project.h:376
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Automatic widget registration helpers for ImGui Test Engine integration.