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