yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sheet_browser_panel.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cstdlib>
6#include <cstring>
7#include <fstream>
8#include <sstream>
9
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_join.h"
15#include "app/gui/core/icons.h"
17#include "app/gui/core/style.h"
22#include "core/project.h"
23#include "imgui/imgui.h"
24#include "imgui/misc/cpp/imgui_stdlib.h"
25#include "rom/rom.h"
26#include "util/file_util.h"
27#include "util/indexed_png.h"
28#include "zelda3/dungeon/room.h"
29#include "zelda3/game_data.h"
31
32namespace yaze {
33namespace editor {
34
36 // Initialize with sensible defaults
37 thumbnail_scale_ = 2.0f;
38 columns_ = 2;
39}
40
41void SheetBrowserPanel::Draw(bool* p_open) {
42 // WindowContent interface - delegate to existing Update() logic
43 (void)Update();
44}
45
47 if (rom_ != inventory_rom_ ||
48 (!inventory_.has_value() && inventory_error_.empty())) {
50 }
52 if (ImGui::BeginTabBar("##SheetBrowserTabs")) {
53 const bool show_sheets = ImGui::BeginTabItem(tr("Sheets"));
54 gui::AutoRegisterLastItem("tab", "Sheets");
55 if (show_sheets) {
58 ImGui::EndTabItem();
59 }
60 const bool show_details = ImGui::BeginTabItem(tr("Details"));
61 gui::AutoRegisterLastItem("tab", "Details");
62 if (show_details) {
63 ImGui::BeginChild("##SheetDetails", ImVec2(0, 0));
65 ImGui::EndChild();
66 ImGui::EndTabItem();
67 }
68 const bool show_png = ImGui::BeginTabItem(tr("PNG"));
69 gui::AutoRegisterLastItem("tab", "PNG");
70 if (show_png) {
71 ImGui::BeginChild("##SheetTransfer", ImVec2(0, 0));
73 ImGui::EndChild();
74 ImGui::EndTabItem();
75 }
76 const bool show_catalog = ImGui::BeginTabItem(tr("Catalog"));
77 gui::AutoRegisterLastItem("tab", "Catalog");
78 if (show_catalog) {
79 ImGui::BeginChild("##CatalogTileTransfer", ImVec2(0, 0));
82 ImGui::EndChild();
83 ImGui::EndTabItem();
84 }
85 ImGui::EndTabBar();
86 }
87 return absl::OkStatus();
88}
89
91 Rom* rom, zelda3::GameData* game_data,
92 std::function<const project::YazeProject*()> project_getter) {
93 if (rom_ != rom || game_data_ != game_data) {
95 }
96 rom_ = rom;
97 game_data_ = game_data;
98 project_getter_ = std::move(project_getter);
99}
100
102 inventory_.reset();
103 inventory_error_.clear();
105 if (rom_ == nullptr || !rom_->is_loaded()) {
106 inventory_error_ = "No ROM loaded";
107 return;
108 }
109 const project::YazeProject* project =
110 project_getter_ ? project_getter_() : nullptr;
111 const auto options = core::BuildGfxSheetInventoryOptions(project);
112 const auto areas = zelda3::CollectOverworldAreaGfx(*rom_, game_data_);
113 const auto rooms = zelda3::CollectRoomGfx(*rom_);
114 auto inventory = zelda3::BuildGfxSheetInventory(*rom_, areas, rooms, options);
115 if (!inventory.ok()) {
116 inventory_error_ = std::string(inventory.status().message());
117 return;
118 }
119 inventory_ = std::move(*inventory);
120}
121
122namespace {
123
124std::string HexList(const std::vector<int>& values) {
125 if (values.empty()) {
126 return "-";
127 }
128 return absl::StrJoin(values, " ", [](std::string* out, int value) {
129 out->append(absl::StrFormat("%02X", value));
130 });
131}
132
133void DrawUsageRow(const char* label, const std::vector<int>& values) {
134 ImGui::TableNextRow();
135 ImGui::TableSetColumnIndex(0);
136 ImGui::TextUnformatted(label);
137 ImGui::TableSetColumnIndex(1);
138 ImGui::Text("%zu", values.size());
139 ImGui::TableSetColumnIndex(2);
140 ImGui::TextWrapped("%s", HexList(values).c_str());
141}
142
143} // namespace
144
145std::string SheetBrowserPanel::SheetLabel(uint16_t sheet) const {
146 const project::YazeProject* project =
147 project_getter_ ? project_getter_() : nullptr;
148 return project != nullptr ? core::GetGraphicsSheetLabel(*project, sheet)
149 : std::string();
150}
151
153 if (label_edit_sheet_ != sheet_id) {
154 label_edit_sheet_ = sheet_id;
155 label_buffer_ = SheetLabel(sheet_id);
156 }
157 const bool can_edit = static_cast<bool>(label_setter_);
158 ImGui::BeginDisabled(!can_edit);
159 ImGui::SetNextItemWidth(-1);
160 const bool submitted = ImGui::InputTextWithHint(
161 "##SheetLabel", tr("Sheet label (stored in the project)"), &label_buffer_,
162 ImGuiInputTextFlags_EnterReturnsTrue);
163 if ((submitted || ImGui::IsItemDeactivatedAfterEdit()) && can_edit) {
164 const absl::Status status = label_setter_(sheet_id, label_buffer_);
165 label_status_ = status.ok() ? "Label set; save the project to keep it"
166 : std::string(status.message());
167 }
168 if (ImGui::SmallButton(ICON_MD_UPLOAD_FILE " Import Spritesets CSV") &&
170 const std::string path = util::FileDialogWrapper::ShowOpenFileDialog();
171 if (!path.empty()) {
172 std::ifstream file(path);
173 std::stringstream text;
174 text << file.rdbuf();
175 auto imported = label_importer_(text.str());
176 label_status_ = imported.ok()
177 ? absl::StrFormat(
178 "Imported %d sheet label(s); existing labels "
179 "were kept",
180 *imported)
181 : std::string(imported.status().message());
182 label_edit_sheet_ = -1; // reload the field
183 }
184 }
186 "Label sprite sheets from Oracle's \"Spritesets\" CSV: a cell like "
187 "\"0x46 Cannon Soldiers\" names sheet 0x46 + 0x73.");
188 ImGui::EndDisabled();
189 if (!can_edit) {
190 ImGui::TextDisabled(
191 tr("Open the project that owns this ROM to edit "
192 "sheet labels."));
193 }
194 if (!label_status_.empty()) {
195 ImGui::TextDisabled("%s", label_status_.c_str());
196 }
197}
198
200 const uint16_t sheet_id = state_->current_sheet_id;
201 if (!ImGui::CollapsingHeader(
202 absl::StrFormat("Sheet 0x%02X usage and free space###SheetUsage",
203 sheet_id)
204 .c_str(),
205 ImGuiTreeNodeFlags_DefaultOpen)) {
206 return;
207 }
208 if (ImGui::SmallButton(ICON_MD_REFRESH " Refresh")) {
210 }
212 "Re-read the ROM buffer (saved sheets, group tables, areas, "
213 "rooms). Unsaved pixel edits are not included.");
214 ImGui::SameLine();
215 ImGui::Checkbox(tr("Show free 16x16"), &show_free_blocks_);
216 HOVER_HINT("Outline empty, unreserved 16x16 blocks on the thumbnails");
217
218 if (!inventory_.has_value()) {
219 ImGui::TextColored(gui::GetWarningColor(), "%s",
220 inventory_error_.empty() ? "Inventory not built"
221 : inventory_error_.c_str());
222 return;
223 }
224 if (sheet_id >= inventory_->sheets.size()) {
225 return;
226 }
227 const auto& entry = inventory_->sheets[sheet_id];
228
229 DrawSheetLabelEditor(sheet_id);
230 ImGui::Text("%s PC 0x%06X", zelda3::GfxSheetInventoryKindName(entry.kind),
231 entry.pc);
232 if (entry.stored_bytes.has_value()) {
233 ImGui::SameLine();
234 ImGui::Text(tr("stored %zu bytes"), *entry.stored_bytes);
235 }
236 if (entry.sprite_value.has_value()) {
237 ImGui::SameLine();
238 ImGui::Text(tr("sprite value 0x%02X"), *entry.sprite_value);
239 }
240 if (!entry.error.empty()) {
241 ImGui::TextColored(gui::GetErrorColor(), "%s", entry.error.c_str());
242 }
243 if (entry.reserved) {
244 ImGui::TextColored(gui::GetErrorColor(),
245 ICON_MD_BLOCK " Reserved: never written, not free");
246 }
247 if (entry.flagged) {
248 ImGui::TextColored(gui::GetWarningColor(),
249 ICON_MD_FLAG " Flagged: ask the owner before use");
250 }
251 if (entry.engine_loaded) {
252 ImGui::TextColored(gui::GetInfoColor(),
253 ICON_MD_MEMORY " Engine-loaded (raw or 2bpp)");
254 }
255 if (!entry.labels.empty()) {
256 ImGui::TextWrapped("%s", absl::StrJoin(entry.labels, "; ").c_str());
257 }
258
259 const auto free_blocks = entry.FreeBlocks();
260 if (entry.has_block_stats) {
261 ImGui::Text(tr("Free 16x16 blocks: %zu of 16 (empty 8x8 tiles: %d)"),
262 free_blocks.size(), entry.empty_8x8);
263 if (!free_blocks.empty()) {
264 ImGui::SameLine();
265 ImGui::TextDisabled("[%s]", HexList(free_blocks).c_str());
266 }
267 if (!entry.reserved_blocks.empty()) {
268 ImGui::Text(tr("Reserved blocks: %s"),
269 absl::StrJoin(entry.reserved_blocks, " ").c_str());
270 }
271 }
272
273 if (ImGui::BeginTable("##SheetUsedBy", 3,
274 ImGuiTableFlags_SizingStretchProp |
275 ImGuiTableFlags_RowBg |
276 ImGuiTableFlags_BordersInnerH)) {
277 ImGui::TableSetupColumn(gui::ColumnLabel(tr("Used by"), "used_by").c_str(),
278 ImGuiTableColumnFlags_WidthFixed);
279 ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed);
280 ImGui::TableSetupColumn(gui::ColumnLabel(tr("Ids (hex)"), "ids").c_str());
281 ImGui::TableHeadersRow();
282 const auto& use = entry.usage;
283 if (entry.sprite_value.has_value()) {
284 DrawUsageRow(tr("Spritesets"), use.spritesets);
285 DrawUsageRow(tr("OW areas (sprites)"), use.ow_areas_sprite);
286 DrawUsageRow(tr("Rooms (sprites)"), use.rooms_sprite);
287 }
288 DrawUsageRow(tr("Main blocksets"), use.main_blocksets);
289 DrawUsageRow(tr("Room blocksets"), use.room_blocksets);
290 DrawUsageRow(tr("OW areas (static)"), use.ow_areas_static);
291 ImGui::EndTable();
292 }
293 if (entry.unreferenced) {
294 ImGui::TextDisabled(tr("No table references this sheet."));
295 }
296 ImGui::Separator();
297}
298
299std::string SheetBrowserPanel::SheetUsageSummary(uint16_t sheet_id) const {
300 if (!inventory_.has_value() || sheet_id >= inventory_->sheets.size()) {
301 return {};
302 }
303 const auto& use = inventory_->sheets[sheet_id].usage;
304 std::vector<std::string> parts;
305 auto add = [&parts](size_t count, const char* what) {
306 if (count > 0) {
307 parts.push_back(absl::StrFormat("%zu %s", count, what));
308 }
309 };
310 add(use.main_blocksets.size(), "main blocksets");
311 add(use.room_blocksets.size(), "room blocksets");
312 add(use.spritesets.size(), "spritesets");
313 add(use.ow_areas_static.size() + use.ow_areas_sprite.size(), "OW areas");
314 add(use.rooms_sprite.size(), "rooms");
315 return parts.empty() ? std::string("no table uses it")
316 : "used by " + absl::StrJoin(parts, ", ");
317}
318
320 if (!save_planner_ || state_->modified_sheets.empty()) {
321 return;
322 }
323 if (!ImGui::CollapsingHeader(
324 absl::StrFormat("Pending graphics save (%zu)###PendingSave",
325 state_->modified_sheets.size())
326 .c_str(),
327 ImGuiTreeNodeFlags_DefaultOpen)) {
328 return;
329 }
330 if (ImGui::SmallButton(ICON_MD_FACT_CHECK " Check save plan")) {
331 auto plan = save_planner_();
333 if (plan.ok()) {
334 save_plan_ = std::move(*plan);
335 save_plan_error_.clear();
336 } else {
337 save_plan_.clear();
338 save_plan_error_ = std::string(plan.status().message());
339 }
340 }
342 "Writes every dirty sheet to a scratch copy of the ROM and decodes it "
343 "back. Saving the ROM does the same for real, and writes nothing if any "
344 "sheet fails.");
345 if (!save_plan_error_.empty()) {
346 ImGui::TextColored(gui::GetErrorColor(), "%s", save_plan_error_.c_str());
347 return;
348 }
349 if (save_plan_.empty()) {
350 ImGui::TextDisabled("%s", tr("Check the plan before saving the ROM."));
351 return;
352 }
354 ImGui::TextColored(gui::GetWarningColor(), "%s",
355 tr("Edits changed since the check; check again."));
356 }
357 for (const auto& entry : save_plan_) {
358 const ImVec4 color =
359 !entry.refusal.empty() ? gui::GetErrorColor()
360 : entry.placement == zelda3::GfxSheetPlacement::kRelocated
363 ImGui::TextColored(color, "%s",
364 DescribeGraphicsSavePlanEntry(entry).c_str());
365 const std::string usage = SheetUsageSummary(entry.sheet_id);
366 if (!usage.empty()) {
367 ImGui::SameLine();
368 ImGui::TextDisabled("%s", usage.c_str());
369 }
370 }
371}
372
373void SheetBrowserPanel::SetPngStatus(std::string message, bool is_error) {
374 png_status_ = std::move(message);
375 png_status_is_error_ = is_error;
376}
377
378absl::StatusOr<zelda3::SheetPalette> SheetBrowserPanel::PngPalette(
379 int sheet_id) {
380 if (png_palette_mode_ == 0) {
382 }
383 if (rom_ == nullptr || game_data_ == nullptr || !rom_->is_loaded()) {
384 return absl::FailedPreconditionError("Open a ROM to use room palettes");
385 }
386 if (png_palette_mode_ == 4) {
387 const auto* target = state_->SourceTarget();
388 if (target == nullptr || state_->game_data != game_data_ ||
389 target->sheet_id != sheet_id || target->palette.size() != 8) {
390 return absl::FailedPreconditionError(
391 "Use Edit Graphics from a source tile to supply its palette");
392 }
393 zelda3::SheetPalette palette;
394 for (int i = 0; i < 8; ++i) {
395 const auto rgb = target->palette[i].rgb();
396 palette[i] = {static_cast<uint8_t>(rgb.x), static_cast<uint8_t>(rgb.y),
397 static_cast<uint8_t>(rgb.z)};
398 }
399 return palette;
400 }
401 if (png_palette_mode_ == 3) {
402 auto context = zelda3::ResolveRoomSheetPalette(
404 if (!context.ok())
405 return context.status();
406 if (sheet_id != context->background.sheets[png_room_slot_]) {
407 return absl::FailedPreconditionError(
408 "Choose a room slot that loads the selected sheet");
409 }
410 return context->colors;
411 }
412 return png_palette_mode_ == 1
418}
419
420void SheetBrowserPanel::DrawPngTransfer(uint16_t sheet_id) {
421 if (!ImGui::CollapsingHeader(tr("PNG export and import###SheetPng"))) {
422 return;
423 }
424 // The Arena holds the last loaded ROM's sheets; only work on this one's.
425 const bool arena_is_ours = game_data_ != nullptr &&
427 if (!arena_is_ours) {
428 ImGui::TextColored(gui::GetWarningColor(), "%s",
429 tr("The sheets in memory belong to another open ROM."));
430 return;
431 }
432
433 const char* modes[] = {tr("Grayscale"), tr("Background row (left half)"),
434 tr("Room sprite row"), tr("Room background slot"),
435 tr("Selected source tile")};
436 ImGui::SetNextItemWidth(180.0f);
437 ImGui::Combo(tr("Palette##png"), &png_palette_mode_, modes,
438 IM_ARRAYSIZE(modes));
439 ImGui::BeginDisabled(png_palette_mode_ == 4);
440 ImGui::SetNextItemWidth(90.0f);
441 ImGui::InputInt(tr("Room##png"), &png_room_, 1, 16,
442 ImGuiInputTextFlags_CharsHexadecimal);
443 png_room_ = std::clamp(png_room_, 0, 295);
444 if (png_palette_mode_ != 0) {
445 ImGui::SameLine();
446 ImGui::SetNextItemWidth(90.0f);
447 ImGui::SliderInt(tr("Row##png"), &png_palette_row_, 0, 7);
448 }
450 "The room picks the palette rows and, for room PNGs, the room's 8 "
451 "background sheets.");
452 ImGui::EndDisabled();
453 if (png_palette_mode_ == 4) {
454 const auto* target = state_->SourceTarget();
455 ImGui::TextWrapped(
456 "%s", target != nullptr
457 ? target->label.c_str()
458 : "Use Edit Graphics from the tile you want to work on.");
459 }
460 if (png_palette_mode_ == 3 && rom_ != nullptr && game_data_ != nullptr) {
461 const auto room = zelda3::LoadRoomHeaderFromRom(rom_, png_room_);
463 room.blockset());
464 std::vector<int> matches;
465 for (int slot = 0; slot < 8; ++slot) {
466 if (set.sheets[slot] == sheet_id)
467 matches.push_back(slot);
468 }
469 if (std::find(matches.begin(), matches.end(), png_room_slot_) ==
470 matches.end()) {
471 png_room_slot_ = matches.size() == 1 ? matches.front() : -1;
472 }
473 auto slot_label = [&](int slot) {
474 return absl::StrFormat(
475 "Slot %d / %s half", slot,
476 zelda3::DungeonBackgroundPaletteOffset(set.main_blockset, slot) == 8
477 ? "right"
478 : "left");
479 };
480 const std::string selected =
481 png_room_slot_ < 0 ? (matches.empty() ? "Sheet not loaded by this room"
482 : "Choose a loaded slot")
483 : slot_label(png_room_slot_);
484 if (ImGui::BeginCombo(tr("Loaded slot##png"), selected.c_str())) {
485 for (int slot : matches) {
486 if (ImGui::Selectable(slot_label(slot).c_str(),
487 slot == png_room_slot_)) {
488 png_room_slot_ = slot;
489 }
490 }
491 ImGui::EndCombo();
492 }
494 "Uses the saved room header and its default entrance graphics. "
495 "Inherited entrance "
496 "choices may differ from a particular gameplay route. PNG "
497 "indices stay 0-7; slot 7's animation overlay is not included.");
498 }
499 ImGui::SetNextItemWidth(320.0f);
500 ImGui::InputTextWithHint("File##PngPath", tr("empty: choose in a dialog"),
501 &png_path_);
503 "PNG path for the export and import buttons. Leave it empty to choose "
504 "the file in a dialog.");
505 ImGui::SetNextItemWidth(90.0f);
506 ImGui::InputInt(tr("At block##png"), &png_first_block_);
507 png_first_block_ = std::clamp(png_first_block_, 0, 15);
509 "The 16x16 block (0-15, 8 per row) where an imported sheet PNG starts; "
510 "pick a free block from the usage list above.");
511
512 const auto& sheets = gfx::Arena::Get().gfx_sheets();
513 const std::vector<uint8_t> pixels = sheet_id < sheets.size()
514 ? sheets[sheet_id].vector()
515 : std::vector<uint8_t>{};
516 util::FileDialogOptions png_filter;
517 png_filter.filters.push_back({"PNG image", "png"});
518 // The typed path wins; otherwise ask with a dialog.
519 auto save_path = [this](const std::string& default_name) {
521 default_name, "png")
522 : png_path_;
523 };
524 auto open_path = [this, &png_filter]() {
525 return png_path_.empty()
527 : png_path_;
528 };
529
530 if (ImGui::Button(ICON_MD_FILE_DOWNLOAD " Export sheet PNG")) {
531 auto palette = PngPalette(sheet_id);
532 auto png = palette.ok()
533 ? ExportSheetPixelsPng(sheet_id, pixels, *palette)
534 : absl::StatusOr<std::vector<uint8_t>>(palette.status());
535 if (!png.ok()) {
536 SetPngStatus(std::string(png.status().message()), true);
537 } else {
538 const std::string path =
539 save_path(absl::StrFormat("sheet_%02X", sheet_id));
540 if (!path.empty()) {
541 const absl::Status status = util::WriteBinaryFile(path, *png);
543 status.ok() ? "Exported " + path : std::string(status.message()),
544 !status.ok());
545 }
546 }
547 }
548 ImGui::SameLine();
549 if (ImGui::Button(ICON_MD_FILE_UPLOAD " Import into sheet")) {
550 auto palette = PngPalette(sheet_id);
551 const std::string path = palette.ok() ? open_path() : std::string();
552 if (!palette.ok()) {
553 SetPngStatus(std::string(palette.status().message()), true);
554 } else if (!path.empty()) {
555 auto bytes = util::ReadBinaryFile(path);
556 auto preview =
557 bytes.ok() ? PreviewSheetPngImport(sheet_id, pixels, *bytes, *palette,
559 : absl::StatusOr<SheetPngImportPreview>(bytes.status());
560 if (!preview.ok()) {
561 png_pending_.clear();
562 SetPngStatus(std::string(preview.status().message()), true);
563 } else {
564 png_pending_ = {*preview};
565 SetPngStatus("Preview of " + path, false);
566 }
567 }
568 }
569
570 ImGui::BeginDisabled(png_palette_mode_ >= 3);
571 if (ImGui::Button(ICON_MD_FILE_DOWNLOAD " Export room PNG")) {
572 auto palette = PngPalette();
573 if (!palette.ok()) {
574 SetPngStatus(std::string(palette.status().message()), true);
575 } else if (rom_ != nullptr && game_data_ != nullptr) {
576 const auto sets =
578 auto png =
580 if (!png.ok()) {
581 SetPngStatus(std::string(png.status().message()), true);
582 } else {
583 const std::string path =
584 save_path(absl::StrFormat("room_%03X_background", png_room_));
585 if (!path.empty()) {
586 const absl::Status status = util::WriteBinaryFile(path, *png);
588 status.ok() ? "Exported " + path + " (sheets as saved in the ROM)"
589 : std::string(status.message()),
590 !status.ok());
591 }
592 }
593 }
594 }
596 "128x256: the room's 8 background sheets in slot order, read from the "
597 "ROM (save pixel edits first to include them). This legacy layout uses "
598 "one uniform palette for all slots.");
599 ImGui::SameLine();
600 if (ImGui::Button(ICON_MD_FILE_UPLOAD " Import room PNG")) {
601 auto palette = PngPalette();
602 const std::string path = palette.ok() ? open_path() : std::string();
603 if (!palette.ok()) {
604 SetPngStatus(std::string(palette.status().message()), true);
605 } else if (!path.empty() && rom_ != nullptr && game_data_ != nullptr) {
606 const auto sets =
608 auto bytes = util::ReadBinaryFile(path);
609 auto previews =
610 bytes.ok()
611 ? PreviewRoomPngImport(*rom_, sets[png_room_], *bytes, *palette)
612 : absl::StatusOr<std::vector<SheetPngImportPreview>>(
613 bytes.status());
614 png_pending_.clear();
615 if (!previews.ok()) {
616 SetPngStatus(std::string(previews.status().message()), true);
617 } else {
618 // The room preview starts from the ROM's sheets, so it would discard
619 // unsaved pixel edits to the same sheets.
620 std::vector<std::string> unsaved;
621 for (const auto& preview : *previews) {
622 if (state_->modified_sheets.count(preview.sheet_id) != 0) {
623 unsaved.push_back(absl::StrFormat("0x%02X", preview.sheet_id));
624 }
625 }
626 if (!unsaved.empty()) {
627 SetPngStatus(absl::StrFormat("Save or discard the pixel edits to "
628 "sheet(s) %s before a room import",
629 absl::StrJoin(unsaved, ", ")),
630 true);
631 } else {
632 png_pending_ = *previews;
633 SetPngStatus("Preview of " + path, false);
634 }
635 }
636 }
637 }
638
639 ImGui::EndDisabled();
640 if (png_palette_mode_ >= 3) {
641 ImGui::TextWrapped(
642 "Room slot colors apply to one sheet. Use Export sheet "
643 "PNG for the selected slot.");
644 }
645 if (!png_status_.empty()) {
646 ImGui::TextColored(
648 png_status_.c_str());
649 }
650 if (png_pending_.empty()) {
651 return;
652 }
653 bool any_change = false;
654 for (const auto& preview : png_pending_) {
655 ImGui::BulletText("%s", DescribeSheetPngImport(preview).c_str());
656 any_change = any_change || !preview.changed_tiles.empty();
657 }
658 ImGui::BeginDisabled(!any_change);
659 if (ImGui::Button(ICON_MD_CHECK " Apply import")) {
660 std::vector<std::string> errors;
661 for (const auto& preview : png_pending_) {
662 const absl::Status status =
664 if (!status.ok()) {
665 errors.push_back(std::string(status.message()));
666 }
667 }
668 SetPngStatus(errors.empty() ? std::string("Imported; save the ROM to write "
669 "the sheets")
670 : absl::StrJoin(errors, "; "),
671 !errors.empty());
672 png_pending_.clear();
673 }
674 ImGui::EndDisabled();
675 ImGui::SameLine();
676 if (ImGui::Button(ICON_MD_CLOSE " Cancel")) {
677 png_pending_.clear();
678 SetPngStatus("Import cancelled", false);
679 }
680}
681
683 ImGui::Text(tr("Search:"));
684 ImGui::SameLine();
685 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetHexInputWidth());
686 if (ImGui::InputText("##SheetSearch", search_buffer_, sizeof(search_buffer_),
687 ImGuiInputTextFlags_CharsHexadecimal)) {
688 // Parse hex input for sheet number
689 if (strlen(search_buffer_) > 0) {
690 int value = static_cast<int>(strtol(search_buffer_, nullptr, 16));
691 if (value >= 0 && value <= 222) {
692 state_->SelectSheet(static_cast<uint16_t>(value));
693 }
694 }
695 }
696 HOVER_HINT("Enter hex sheet number (00-DE)");
697
698 ImGui::SameLine();
699 if (ImGui::Button(ICON_MD_TUNE "##SheetFilters")) {
700 ImGui::OpenPopup("Sheet filters");
701 }
702 gui::AutoRegisterLastItem("button", "Sheet filters");
703 HOVER_HINT("Filter range, modified sheets, and thumbnail size");
704 if (ImGui::BeginPopup("Sheet filters")) {
705 ImGui::SetNextItemWidth(70);
706 ImGui::DragInt("First", &filter_min_, 1.0f, 0, 222, "%02X");
707 ImGui::SetNextItemWidth(70);
708 ImGui::DragInt("Last", &filter_max_, 1.0f, 0, 222, "%02X");
709 filter_min_ = std::clamp(filter_min_, 0, 222);
710 filter_max_ = std::clamp(filter_max_, filter_min_, 222);
711 ImGui::Checkbox(tr("Modified only"), &show_only_modified_);
712 ImGui::Separator();
714 ImGui::EndPopup();
715 }
716
717 // Display only: recolors the Arena sheets, never the ROM or project.
718 ImGui::AlignTextToFramePadding();
719 ImGui::TextUnformatted(tr("Palette:"));
720 HOVER_HINT("Show every sheet in this palette; display only");
721 ImGui::SameLine();
722 ImGui::PushID("BrowserPalette");
726 ImGui::PopID();
727}
728
730 if (ImGui::Button(ICON_MD_SELECT_ALL " Select All")) {
731 for (int i = filter_min_; i <= filter_max_; i++) {
732 state_->selected_sheets.insert(static_cast<uint16_t>(i));
733 }
734 }
735 ImGui::SameLine();
736 if (ImGui::Button(ICON_MD_DESELECT " Clear")) {
737 state_->selected_sheets.clear();
738 }
739
740 if (!state_->selected_sheets.empty()) {
741 ImGui::SameLine();
742 ImGui::Text(tr("(%zu selected)"), state_->selected_sheets.size());
743 }
744
745 // Thumbnail size slider
746 ImGui::SameLine();
747 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
748 ImGui::SliderFloat("##Scale", &thumbnail_scale_, 1.0f, 4.0f, "%.1fx");
749}
750
752 ImGui::BeginChild("##SheetGridChild", ImVec2(0, 0), true,
753 ImGuiWindowFlags_AlwaysVerticalScrollbar);
754
755 auto& sheets = gfx::Arena::Get().gfx_sheets();
756
757 // Calculate thumbnail size
758 const float fit_scale =
759 std::min(thumbnail_scale_,
760 std::max(0.25f, (ImGui::GetContentRegionAvail().x - 12) / 128));
761 const float thumb_width = 128 * fit_scale;
762 const float thumb_height = 32 * fit_scale;
763 const float padding = 4.0f;
764
765 // Calculate columns based on available width
766 float available_width = ImGui::GetContentRegionAvail().x;
767 columns_ = std::max(
768 1, static_cast<int>(available_width / (thumb_width + padding * 2)));
769
770 int col = 0;
771 for (int i = filter_min_; i <= filter_max_ && i < zelda3::kNumGfxSheets;
772 i++) {
773 // Filter by modification state if enabled
775 state_->modified_sheets.find(static_cast<uint16_t>(i)) ==
776 state_->modified_sheets.end()) {
777 continue;
778 }
779
780 if (col > 0) {
781 ImGui::SameLine();
782 }
783
784 ImGui::PushID(i);
785 DrawSheetThumbnail(i, sheets[i]);
786 ImGui::PopID();
787
788 col++;
789 if (col >= columns_) {
790 col = 0;
791 }
792 }
793
794 ImGui::EndChild();
795}
796
798 const float fit_scale =
799 std::min(thumbnail_scale_,
800 std::max(0.25f, (ImGui::GetContentRegionAvail().x - 12) / 128));
801 const float thumb_width = 128 * fit_scale;
802 const float thumb_height = 32 * fit_scale;
803
804 bool is_selected =
805 state_->current_sheet_id == static_cast<uint16_t>(sheet_id);
806 bool is_multi_selected =
807 state_->selected_sheets.count(static_cast<uint16_t>(sheet_id)) > 0;
808 bool is_modified =
809 state_->modified_sheets.count(static_cast<uint16_t>(sheet_id)) > 0;
810
811 // Selection highlight
812 std::optional<gui::StyleColorGuard> sel_bg_guard;
813 if (is_selected) {
814 ImVec4 sel_bg = gui::GetSelectedColor();
815 sel_bg.w = 0.3f;
816 sel_bg_guard.emplace(ImGuiCol_ChildBg, sel_bg);
817 } else if (is_multi_selected) {
818 ImVec4 multi_bg = gui::GetModifiedColor();
819 multi_bg.w = 0.3f;
820 sel_bg_guard.emplace(ImGuiCol_ChildBg, multi_bg);
821 }
822
823 ImGui::BeginChild(absl::StrFormat("##Sheet%02X", sheet_id).c_str(),
824 ImVec2(thumb_width + 8, thumb_height + 24), true,
825 ImGuiWindowFlags_NoScrollbar);
826
827 gui::BitmapPreviewOptions preview_opts;
828 preview_opts.canvas_size = ImVec2(thumb_width + 1, thumb_height + 1);
829 preview_opts.dest_pos = ImVec2(2, 2);
830 preview_opts.dest_size = ImVec2(thumb_width - 2, thumb_height - 2);
831 preview_opts.grid_step = 8.0f * fit_scale;
832 preview_opts.draw_context_menu = false;
833 preview_opts.ensure_texture = true;
834
835 gui::CanvasFrameOptions frame_opts;
836 frame_opts.canvas_size = preview_opts.canvas_size;
837 frame_opts.draw_context_menu = preview_opts.draw_context_menu;
838 frame_opts.draw_grid = preview_opts.draw_grid;
839 frame_opts.grid_step = preview_opts.grid_step;
840 frame_opts.draw_overlay = preview_opts.draw_overlay;
841 frame_opts.render_popups = preview_opts.render_popups;
842
843 {
845 auto rt = gui::BeginCanvas(thumbnail_canvas_, frame_opts);
846 gui::DrawBitmapPreview(rt, bitmap, preview_opts);
847
848 // Sheet label with modification indicator
849 std::string label = absl::StrFormat("%02X", sheet_id);
850 if (is_modified) {
851 label += "*";
852 }
853
854 // Draw label with background
855 ImVec2 text_pos = ImGui::GetCursorScreenPos();
856 ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
858 ImVec2(2, 2), ImVec2(text_size.x + 4, text_size.y + 2),
859 is_modified ? ImGui::GetColorU32(IM_COL32(180, 100, 0, 200))
860 : ImGui::GetColorU32(IM_COL32(0, 100, 0, 180)));
861
863 ImVec2(4, 2), label,
864 is_modified ? ImGui::GetColorU32(IM_COL32(255, 200, 100, 255))
865 : ImGui::GetColorU32(IM_COL32(150, 255, 150, 255)));
866 if (is_modified) {
867 // Dirty dot: unsaved pixel edits in this sheet.
868 const ImVec2 dot =
869 ImVec2(thumbnail_canvas_.zero_point().x + thumb_width - 8,
870 thumbnail_canvas_.zero_point().y + thumb_height - 8);
871 ImGui::GetWindowDrawList()->AddCircleFilled(
872 dot, 4.0f, ImGui::GetColorU32(gui::GetModifiedColor()));
873 }
874
875 if (inventory_.has_value() &&
876 sheet_id < static_cast<int>(inventory_->sheets.size())) {
877 const auto& entry = inventory_->sheets[sheet_id];
878 const float block_w = preview_opts.dest_size.x / 8.0f;
879 const float block_h = preview_opts.dest_size.y / 2.0f;
880 if (show_free_blocks_) {
881 for (int block : entry.FreeBlocks()) {
883 ImVec2(preview_opts.dest_pos.x + (block % 8) * block_w,
884 preview_opts.dest_pos.y + (block / 8) * block_h),
885 ImVec2(block_w, block_h),
886 entry.flagged ? ImGui::GetColorU32(IM_COL32(230, 180, 40, 70))
887 : ImGui::GetColorU32(IM_COL32(60, 200, 90, 70)));
888 }
889 }
890 const char* badge =
891 entry.reserved ? "RES" : (entry.flagged ? "ASK" : nullptr);
892 if (badge != nullptr) {
893 const ImVec2 badge_size = ImGui::CalcTextSize(badge);
894 const ImVec2 badge_pos(thumb_width - badge_size.x - 4, 2);
896 badge_pos, ImVec2(badge_size.x + 4, badge_size.y + 2),
897 entry.reserved ? ImGui::GetColorU32(IM_COL32(170, 30, 30, 220))
898 : ImGui::GetColorU32(IM_COL32(170, 120, 20, 220)));
900 ImVec2(badge_pos.x + 2, badge_pos.y), badge,
901 ImGui::GetColorU32(IM_COL32(255, 255, 255, 255)));
902 }
903 }
904 gui::EndCanvas(thumbnail_canvas_, rt, frame_opts);
905 }
906
907 // Click handling
908 if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
909 if (ImGui::GetIO().KeyCtrl) {
910 // Ctrl+click for multi-select
911 if (is_multi_selected) {
912 state_->selected_sheets.erase(static_cast<uint16_t>(sheet_id));
913 } else {
914 state_->selected_sheets.insert(static_cast<uint16_t>(sheet_id));
915 }
916 } else {
917 // Normal click to select
918 state_->SelectSheet(static_cast<uint16_t>(sheet_id));
919 }
920 }
921
922 // Double-click to open in new tab
923 if (ImGui::IsItemHovered() &&
924 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
925 state_->open_sheets.insert(static_cast<uint16_t>(sheet_id));
926 }
927
928 ImGui::EndChild();
929
930 sel_bg_guard.reset();
931
932 // Tooltip with sheet info
933 if (ImGui::IsItemHovered()) {
934 ImGui::BeginTooltip();
935 ImGui::Text(tr("Sheet: 0x%02X (%d)"), sheet_id, sheet_id);
936 if (const std::string label = SheetLabel(static_cast<uint16_t>(sheet_id));
937 !label.empty()) {
938 ImGui::TextUnformatted(label.c_str());
939 }
940 if (bitmap.is_active()) {
941 ImGui::Text(tr("Size: %dx%d"), bitmap.width(), bitmap.height());
942 ImGui::Text(tr("Depth: %d bpp"), bitmap.depth());
943 } else {
944 ImGui::Text(tr("(Inactive)"));
945 }
946 if (is_modified) {
947 ImGui::TextColored(gui::GetModifiedColor(), tr("Modified"));
948 }
949 if (inventory_.has_value() &&
950 sheet_id < static_cast<int>(inventory_->sheets.size())) {
951 const auto& entry = inventory_->sheets[sheet_id];
952 if (entry.reserved) {
953 ImGui::TextColored(gui::GetErrorColor(), tr("Reserved"));
954 } else if (entry.flagged) {
955 ImGui::TextColored(gui::GetWarningColor(), tr("Flagged"));
956 }
957 if (entry.has_block_stats) {
958 ImGui::Text(tr("Free 16x16 blocks: %zu"), entry.FreeBlocks().size());
959 }
960 if (!entry.labels.empty()) {
961 ImGui::TextUnformatted(absl::StrJoin(entry.labels, "; ").c_str());
962 }
963 }
964 ImGui::EndTooltip();
965 }
966}
967
968} // namespace editor
969} // namespace yaze
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
void SelectSheet(uint16_t sheet_id)
Select a sheet for editing.
const GraphicsEditTarget * SourceTarget() const
void DrawBatchOperations()
Draw batch operation buttons.
absl::StatusOr< zelda3::SheetPalette > PngPalette(int sheet_id=-1)
std::function< const project::YazeProject *()> project_getter_
void Draw(bool *p_open) override
Draw the sheet browser UI.
const std::optional< zelda3::GfxSheetInventory > & inventory() const
void DrawPngTransfer(uint16_t sheet_id)
std::optional< zelda3::GfxSheetInventory > inventory_
std::string SheetUsageSummary(uint16_t sheet_id) const
void SetDataSources(Rom *rom, zelda3::GameData *game_data, std::function< const project::YazeProject *()> project_getter)
Where free-block and "used by" data come from. The project getter may return null (no project open).
void RefreshInventory()
Rebuild the sheet inventory from the ROM buffer. Runs lazily on the first draw and when the ROM chang...
CatalogTileBrowserState catalog_browser_
void DrawSheetLabelEditor(uint16_t sheet_id)
void DrawSelectedSheetInfo()
Storage, reserved state, free 16x16 blocks and "used by" for the current sheet.
void SetPngStatus(std::string message, bool is_error)
void DrawSearchBar()
Draw the search/filter bar.
void Initialize()
Initialize the panel.
void DrawSheetThumbnail(int sheet_id, gfx::Bitmap &bitmap)
Draw a single sheet thumbnail.
std::vector< GraphicsSavePlanEntry > save_plan_
std::vector< SheetPngImportPreview > png_pending_
std::string SheetLabel(uint16_t sheet) const
void DrawSheetGrid()
Draw the sheet grid view.
absl::Status Update()
Legacy Update method for backward compatibility.
const void * gfx_sheets_owner() const
Definition arena.h:216
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
bool is_active() const
Definition bitmap.h:414
int height() const
Definition bitmap.h:399
int width() const
Definition bitmap.h:398
int depth() const
Definition bitmap.h:400
void AddTextAt(ImVec2 local_pos, const std::string &text, uint32_t color)
Definition canvas.cc:2094
void AddRectFilledAt(ImVec2 local_top_left, ImVec2 size, uint32_t color)
Definition canvas.cc:2083
auto zero_point() const
Definition canvas.h:364
CanvasConfig & GetConfig()
Definition canvas.h:243
static float GetHexInputWidth()
static float GetSliderWidth()
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
3bpp graphics sheets to and from indexed PNGs.
#define ICON_MD_BLOCK
Definition icons.h:269
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_FILE_DOWNLOAD
Definition icons.h:744
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_FACT_CHECK
Definition icons.h:721
#define ICON_MD_FLAG
Definition icons.h:784
#define ICON_MD_UPLOAD_FILE
Definition icons.h:2049
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DESELECT
Definition icons.h:540
#define ICON_MD_CLOSE
Definition icons.h:418
#define HOVER_HINT(string)
Definition macro.h:24
zelda3::GfxSheetInventoryOptions BuildGfxSheetInventoryOptions(const project::YazeProject *project)
std::string GetGraphicsSheetLabel(const project::YazeProject &project, uint16_t sheet)
void DrawUsageRow(const char *label, const std::vector< int > &values)
std::string HexList(const std::vector< int > &values)
absl::StatusOr< std::vector< uint8_t > > ExportSheetPixelsPng(uint16_t sheet_id, const std::vector< uint8_t > &indexed_pixels, const zelda3::SheetPalette &palette)
void DrawCatalogTileBrowser(CatalogTileBrowserState &browser, GraphicsEditorState &destination, UndoManager *undo_manager, const std::string &destination_usage)
std::string DescribeSheetPngImport(const SheetPngImportPreview &preview)
"Sheet 0x20: blocks 0, 3 (5 tiles)" for the preview list.
void DrawGraphicsPreviewPaletteSelector(GraphicsEditorState &state, GraphicsPaletteSelectorLayout layout, GraphicsPaletteSelectorScope scope)
std::string DescribeGraphicsSavePlanEntry(const GraphicsSavePlanEntry &entry)
"Sheet 0x92: in place at 0x0AED16, 1476 -> 1473 bytes".
absl::StatusOr< std::vector< SheetPngImportPreview > > PreviewRoomPngImport(const Rom &rom, const zelda3::RoomBackgroundSet &set, const std::vector< uint8_t > &png_bytes, const zelda3::SheetPalette &palette)
absl::StatusOr< SheetPngImportPreview > PreviewSheetPngImport(uint16_t sheet_id, const std::vector< uint8_t > &indexed_pixels, const std::vector< uint8_t > &png_bytes, const zelda3::SheetPalette &palette, int first_block)
absl::Status ApplySheetPngImport(const SheetPngImportPreview &preview, GraphicsEditorState &state, UndoManager *undo_manager)
void EndCanvas(Canvas &canvas)
std::string ColumnLabel(absl::string_view text, absl::string_view stable_id)
void DrawBitmapPreview(const CanvasRuntime &rt, gfx::Bitmap &bitmap, const BitmapPreviewOptions &options)
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
ImVec4 GetSelectedColor()
Definition ui_helpers.cc:99
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImVec4 GetModifiedColor()
ImVec4 GetInfoColor()
Definition ui_helpers.cc:64
absl::Status WriteBinaryFile(const std::string &path, const std::vector< uint8_t > &bytes)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFile(const std::string &path)
std::vector< RoomBackgroundSet > ResolveAllRoomBackgroundSets(const Rom &rom, const GameData &data)
All 296 rooms' background sets (header bytes read from the ROM).
absl::StatusOr< SheetPalette > RoomSpriteSheetPalette(Rom &rom, GameData &data, int room, int row, bool high_half)
absl::StatusOr< RoomSheetPaletteContext > ResolveRoomSheetPalette(Rom &rom, GameData &data, int room, int slot, int row)
absl::StatusOr< std::vector< uint8_t > > ExportRoomBackgroundPng(const Rom &rom, const RoomBackgroundSet &set, const SheetPalette &palette)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
const char * GfxSheetInventoryKindName(GfxSheetStorageKind kind)
RoomBackgroundSet ResolveRoomBackgroundSet(const GameData &data, int room, uint8_t header_blockset)
absl::StatusOr< GfxSheetInventory > BuildGfxSheetInventory(const Rom &rom, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms, const GfxSheetInventoryOptions &options)
bool IsHighHalfSpriteSheet(int sheet)
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
constexpr uint32_t kNumGfxSheets
Definition game_data.h:28
int DungeonBackgroundPaletteOffset(uint8_t main_blockset, int destination_slot)
std::array< SheetColor, 8 > SheetPalette
Colors for 3bpp indices 0-7. Index 0 is exported as transparent.
SheetPalette GrayscaleSheetPalette()
absl::StatusOr< SheetPalette > RoomBackgroundSheetPalette(Rom &rom, GameData &data, int room, int row)
std::optional< CatalogTileTransferPlan > pending
std::optional< float > grid_step
std::optional< float > grid_step
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
std::vector< FileDialogFilter > filters
Definition file_util.h:18
Automatic widget registration helpers for ImGui Test Engine integration.