12#include <unordered_map>
15#include "absl/status/status.h"
16#include "absl/strings/match.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
40#include "imgui/imgui_internal.h"
41#include "imgui/misc/cpp/imgui_stdlib.h"
53using ImGui::BeginChild;
54using ImGui::BeginTable;
58using ImGui::InputTextMultiline;
62using ImGui::Separator;
63using ImGui::TableHeadersRow;
64using ImGui::TableNextColumn;
65using ImGui::TableSetupColumn;
67using ImGui::TextWrapped;
70 ImGuiTableFlags_Borders |
71 ImGuiTableFlags_Resizable;
89 window_manager->RegisterWindowContent(
91 window_manager->RegisterWindowContent(
93 window_manager->RegisterWindowContent(
94 std::make_unique<FontAtlasPanel>([
this]() {
98 window_manager->RegisterWindowContent(
102 window_manager->OpenWindow(session_id,
"message.message_list");
109 const int message_read_limit =
static_cast<int>(
110 std::min(
static_cast<size_t>(
rom()->size()),
111 static_cast<size_t>(std::numeric_limits<int>::max())));
114 LOG_INFO(
"MessageEditor",
"Loaded %zu messages from ROM",
139 LOG_ERROR(
"MessageEditor",
"No messages found in ROM!");
150 const int vanilla_count =
static_cast<int>(
list_of_texts_.size());
155 expanded_base_id, expanded_count);
159 if (!resolved.has_value() && expanded_count == 0 &&
163 const int start = range.first;
164 const int end = range.second;
165 const size_t rom_size =
rom_->
size();
166 if (start >= 0 && end >= start &&
static_cast<size_t>(end) < rom_size) {
170 "OpenMessageById: expanded load skipped/failed: %s",
171 std::string(status.message()).c_str());
175 expanded_base_id, expanded_count);
178 "OpenMessageById: expanded region out of bounds (0x%X-0x%X, "
180 start, end, rom_size);
184 if (!resolved.has_value()) {
191 "message.message_list");
193 "message.message_editor");
196 if (!resolved->is_expanded) {
197 const int idx = resolved->index;
198 if (idx < 0 || idx >= vanilla_count) {
207 const int parsed_idx = resolved->display_id;
208 if (parsed_idx >= 0 &&
227 const int idx = resolved->index;
228 if (idx < 0 || idx >= expanded_count) {
237 const int parsed_idx = resolved->display_id;
238 if (parsed_idx >= 0 &&
260 if (layout.first_expanded_id != 0) {
261 base_id =
static_cast<int>(layout.first_expanded_id);
267 base_id = std::max(base_id,
static_cast<int>(
list_of_texts_.size()));
274 return absl::OkStatus();
280 return absl::OkStatus();
285 "Expanded source unavailable: %s. Vanilla dialogue remains available.",
291 return absl::OkStatus();
298 return absl::OkStatus();
309 int* cursor =
nullptr;
310 int* select_start =
nullptr;
311 int* select_end =
nullptr;
316 if (state ==
nullptr) {
319 if (state->select_start !=
nullptr && state->select_end !=
nullptr &&
320 *state->select_start >= 0) {
322 const int end = std::max(start, *state->select_end);
323 data->CursorPos = std::min(end, data->BufTextLen);
324 data->SelectionStart = std::min(start, data->BufTextLen);
325 data->SelectionEnd = data->CursorPos;
326 *state->select_start = -1;
327 *state->select_end = -1;
329 if (state->cursor !=
nullptr) {
330 *state->cursor = data->CursorPos;
336 ImGuiContext* context = ImGui::GetCurrentContext();
337 if (context ==
nullptr || context->NavWindow ==
nullptr) {
340 ImGuiWindow* root = context->NavWindow->RootWindow !=
nullptr
341 ? context->NavWindow->RootWindow
342 : context->NavWindow;
343 return root->Name !=
nullptr &&
344 std::strstr(root->Name,
"message.message") !=
nullptr;
350 if (!MessageSurfaceFocused()) {
353 const ImGuiIO& io = ImGui::GetIO();
354 if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift &&
355 ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
358 if (io.KeyAlt && !io.KeyCtrl && !io.KeyShift &&
359 ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
362 if (!io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_F3,
false)) {
372 if (status_bar ==
nullptr) {
387 absl::StrFormat(
"%d/%d", shown, total));
407 if (display_id < 0 ||
415 bool apply_filter)
const {
416 std::vector<MessageRow> rows;
419 const auto consider = [&](
int display_id,
bool expanded, uint32_t address) {
426 rows.push_back(
MessageRow{display_id, expanded, address});
429 consider(message.ID,
false,
static_cast<uint32_t
>(message.Address));
433 static_cast<uint32_t
>(message.Address));
443 const int pos = std::clamp(
editor_cursor_, 0,
static_cast<int>(text.size()));
444 text.insert(
static_cast<size_t>(pos), token);
448 ImGui::ClearActiveID();
468 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
469 if (rows[i].display_id == current_id &&
476 std::clamp(current + delta, 0,
static_cast<int>(rows.size()) - 1);
477 if (next == current) {
503 const int end =
static_cast<int>(offset +
search_text_.size());
509 wrapped ?
" (wrapped)" :
"");
529 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
530 if (rows[i].display_id == current_id &&
537 const int count =
static_cast<int>(rows.size());
538 const size_t cursor =
static_cast<size_t>(std::clamp(
543 auto match_at = [&](
int row_index,
size_t bound,
544 bool from_start) -> std::optional<size_t> {
545 const std::string_view text =
551 const size_t before = from_start ? std::string::npos : bound;
557 match_at(start, forward ? forward_from : backward_before,
false)) {
560 for (
int step = 1; step < count; ++step) {
561 const int row_index =
562 forward ? (start + step) % count : (start - step + count) % count;
563 if (
auto found = match_at(row_index, 0,
true)) {
570 const bool can_wrap = forward ? forward_from > 0 :
true;
572 if (
auto found = match_at(start, 0,
true)) {
602 ImGui::ClearActiveID();
614 ImGui::PushID(
"FindReplace");
616 ImGui::SetKeyboardFocusHere();
620 std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f - 8.0f);
621 ImGui::SetNextItemWidth(half);
622 if (ImGui::InputTextWithHint(
"##find",
"Find in dialogue", &
search_text_)) {
626 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
627 ImGui::InputTextWithHint(
"##replace",
"Replace with", &
replace_text_);
629 if (ImGui::Button(tr(
"Find Next"))) {
633 if (ImGui::Button(tr(
"Previous"))) {
637 if (ImGui::Button(tr(
"Find All"))) {
646 matches, matches == 1 ?
"" :
"s");
651 "message.message_list");
656 if (ImGui::Button(tr(
"Replace"))) {
670 if (ImGui::Button(tr(
"Replace All"))) {
674 count == 1 ?
"" :
"s");
679 if (ImGui::SmallButton(tr(
"Close"))) {
684 if (ImGui::IsItemHovered()) {
685 ImGui::SetTooltip(
"Applies to Find Next, Previous, and Replace.");
689 if (ImGui::IsItemHovered()) {
690 ImGui::SetTooltip(
"Match only when both sides are not letters or digits.");
713 if (display_id < 0 ||
725 if (display_id < 0) {
741 const float label_h = ImGui::GetFrameHeightWithSpacing();
742 const float text_h = std::max(48.0f, height - label_h);
744 auto draw_editor = [&](
float editor_height) {
748 ImVec2(-1.0f, editor_height),
749 ImGuiInputTextFlags_CallbackAlways,
750 MessageInputCallback, &input_state)) {
753 if (ImGui::IsItemDeactivatedAfterEdit()) {
758 if (!show_reference) {
763 const bool side_by_side = ImGui::GetContentRegionAvail().x >= 640.0f;
765 ImGui::BeginTable(
"##DialogueColumns", 2, ImGuiTableFlags_Resizable,
766 ImVec2(0.0f, height))) {
768 ImGuiTableColumnFlags_WidthStretch, 0.45f);
770 ImGuiTableColumnFlags_WidthStretch, 0.55f);
771 ImGui::TableNextRow();
772 ImGui::TableNextColumn();
773 ImGui::TextDisabled(tr(
"Loaded"));
775 ImVec2(-1.0f, text_h),
776 ImGuiInputTextFlags_ReadOnly);
777 ImGui::TableNextColumn();
778 ImGui::TextDisabled(tr(
"Editing"));
784 ImGui::TextDisabled(tr(
"Loaded"));
785 const float reference_h = std::clamp(height * 0.34f, 64.0f, 140.0f);
787 ImVec2(-1.0f, reference_h),
788 ImGuiInputTextFlags_ReadOnly);
789 ImGui::TextDisabled(tr(
"Editing"));
790 draw_editor(std::max(48.0f, height - reference_h - label_h * 2.0f));
812 return {fallback_start, fallback_end};
815 if (layout.data_start == 0 || layout.data_end < layout.data_start) {
816 return {fallback_start, fallback_end};
818 const int start =
static_cast<int>(
SnesToPc(layout.data_start));
819 const int end =
static_cast<int>(
SnesToPc(layout.data_end));
821 return {fallback_start, fallback_end};
829 "The draft changed after preview. Preview again before publishing.";
838absl::StatusOr<std::filesystem::path>
841 return absl::FailedPreconditionError(
842 "The project hack manifest does not define messages.source");
845 .
source->canonical_bundle_path;
846 const std::filesystem::path relative(configured);
847 if (relative.empty() || relative.is_absolute() || relative.has_root_name()) {
848 return absl::InvalidArgumentError(
849 "Canonical message bundle must be a project-relative path");
851 std::error_code error;
852 const std::filesystem::path descriptor =
855 return absl::FailedPreconditionError(
856 absl::StrFormat(
"Cannot resolve project descriptor %s: %s",
859 const std::filesystem::path root =
860 std::filesystem::canonical(descriptor.parent_path(), error);
862 return absl::FailedPreconditionError(
863 absl::StrFormat(
"Cannot resolve project root for %s: %s",
866 const std::filesystem::path joined = (root / relative).lexically_normal();
867 const std::filesystem::path from_root = joined.lexically_relative(root);
868 if (from_root.empty() || from_root.is_absolute() ||
869 *from_root.begin() ==
"..") {
870 return absl::InvalidArgumentError(
871 "Canonical message bundle must stay inside the project root");
877 const std::filesystem::path& path)
const {
878 std::ifstream input(path, std::ios::binary);
879 if (!input.is_open()) {
880 return absl::NotFoundError(absl::StrFormat(
881 "Cannot read canonical message bundle: %s", path.string()));
883 const std::string content((std::istreambuf_iterator<char>(input)),
884 std::istreambuf_iterator<char>());
885 if (!input.good() && !input.eof()) {
886 return absl::InternalError(absl::StrFormat(
887 "Cannot read canonical message bundle: %s", path.string()));
894 if (!canonical_or.ok() || path.empty()) {
897 std::error_code error;
898 const auto incoming = std::filesystem::weakly_canonical(path, error);
903 std::filesystem::weakly_canonical(canonical_or.value(), error);
904 return !error && incoming == target;
948 std::error_code error;
949 std::filesystem::path directory = std::filesystem::temp_directory_path(error);
951 directory = std::filesystem::current_path(error);
953 return (directory / absl::StrFormat(
"yaze-message-source-sync-%zu.json",
976 "The project hack manifest does not define messages.source";
982 "Load the canonical expanded bundle before previewing publication";
987 const auto canonical_sha_or = [&]() -> absl::StatusOr<std::string> {
990 return path_or.status();
994 if (!canonical_sha_or.ok()) {
1001 "Canonical source changed on disk after it was loaded. Reload the "
1002 "canonical bundle before preview. This draft was not published.";
1008 const auto write_status =
1010 if (!write_status.ok()) {
1012 "Could not write the preview bundle: %s", write_status.message());
1016 const auto draft_sha_or =
1018 if (!draft_sha_or.ok()) {
1026 if (!result_or.ok()) {
1038 "%s. %d update%s, %zu / %zu bytes. SHA %s",
1039 result.
changed ?
"Source would change" :
"Source already matches",
1065 const auto write_status =
1067 if (!write_status.ok()) {
1069 "Could not write the publication bundle: %s", write_status.message());
1073 const auto draft_sha_or =
1079 "The draft changed after preview. Preview again before publishing. "
1080 "Nothing was written.";
1086 const auto canonical_sha_or =
1087 canonical_path_or.ok()
1089 : absl::StatusOr<std::string>(canonical_path_or.status());
1090 if (!canonical_sha_or.ok() ||
1096 "Canonical source changed after preview. Reload the canonical bundle "
1097 "and preview again. This draft was not published.";
1103 options.
write =
true;
1109 if (!result_or.ok()) {
1118 result_or->wrote ?
"Published the canonical bundle and ASM include. "
1119 "Rebuild before the patched ROM shows this text."
1120 :
"Canonical source already matched; nothing written";
1123 absl::StrFormat(
"%s\n%s", result_or->canonical_bundle_path.string(),
1124 result_or->generated_asm_include_path.string());
1132 if (ImGui::Button(tr(
"Preview Publication"))) {
1136 (ImGui::GetItemFlags() & ImGuiItemFlags_Disabled) != 0;
1137 ImGui::EndDisabled();
1140 if (ImGui::Button(tr(
"Publish"))) {
1141 ImGui::OpenPopup(
"Publish message source###PublishMessageSource");
1144 (ImGui::GetItemFlags() & ImGuiItemFlags_Disabled) != 0;
1145 ImGui::EndDisabled();
1155 tr(
"Load a project whose hack manifest defines messages.source to "
1156 "publish expanded dialogue as a canonical bundle and ASM include."));
1160 const auto& source =
1162 ImGui::TextDisabled(
"%s", source.canonical_bundle_path.c_str());
1163 ImGui::TextDisabled(
"%s", source.generated_asm_include_path.c_str());
1166 tr(
"Preview checks capacity and the current source SHA without writing. "
1167 "Publish updates the bundle and generated include, not the ROM."));
1171 if (ImGui::BeginPopupModal(
"Publish message source###PublishMessageSource",
1172 nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
1174 "%s", tr(
"Write the expanded messages into the canonical bundle and "
1175 "generated ASM include. The ROM is not modified."));
1179 if (ImGui::Button(tr(
"Publish now"))) {
1181 ImGui::CloseCurrentPopup();
1184 if (ImGui::Button(tr(
"Cancel"))) {
1185 ImGui::CloseCurrentPopup();
1199 ImGui::BeginDisabled(!can_navigate);
1203 ImGui::EndDisabled();
1204 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1205 ImGui::SetTooltip(
"Previous message (Alt+Up)");
1208 ImGui::BeginDisabled(!can_navigate);
1212 ImGui::EndDisabled();
1213 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1214 ImGui::SetTooltip(
"Next message (Alt+Down)");
1236 : tr(
"Show reference"))) {
1239 if (ImGui::IsItemHovered()) {
1240 ImGui::SetTooltip(
"Show the text from when this message was loaded.");
1245 if (ImGui::Button(tr(
"Accept"))) {
1248 ImGui::EndDisabled();
1249 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1251 "Make the current draft the loaded reference for this message.");
1258 if (ImGui::IsItemHovered()) {
1259 ImGui::SetTooltip(
"Find and replace (F3 finds the next match)");
1262 if (ImGui::Button(tr(
"Optimize"))) {
1265 if (ImGui::IsItemHovered()) {
1267 "Replace dictionary phrases in this message with [D:XX] tokens.");
1275 if (!path.empty()) {
1279 if (ImGui::IsItemHovered()) {
1280 ImGui::SetTooltip(
"Import message bundle");
1285 if (!path.empty()) {
1290 absl::StrFormat(
"Export failed: %s", status.message());
1298 if (ImGui::IsItemHovered()) {
1299 ImGui::SetTooltip(
"Export message bundle");
1303 ImGui::SetNextItemWidth(
1304 std::max(120.0f, ImGui::GetContentRegionAvail().x - 120.0f));
1305 ImGui::InputTextWithHint(
"##message_filter",
"Filter by text, ID, or address",
1308 ImGui::SetNextItemWidth(110.0f);
1310 "All\0Vanilla\0Expanded\0");
1320 ImGui::TextDisabled(
"%d / %d",
static_cast<int>(rows.size()), total);
1324 "Load a ROM to edit dialogue.",
1325 nullptr,
nullptr,
true});
1331 empty.
title =
"No matches";
1332 empty.
detail =
"No messages match this filter.";
1343 constexpr ImGuiTableFlags kListFlags =
1344 ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders |
1345 ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg |
1346 ImGuiTableFlags_ScrollY;
1347 if (!BeginTable(
"##MessagesTable", 4, kListFlags, ImVec2(0, 0))) {
1350 TableSetupColumn(
"ID", ImGuiTableColumnFlags_WidthFixed, 64);
1351 TableSetupColumn(
"Bank", ImGuiTableColumnFlags_WidthFixed, 84);
1352 TableSetupColumn(
"Contents", ImGuiTableColumnFlags_WidthStretch);
1353 TableSetupColumn(
"Address", ImGuiTableColumnFlags_WidthFixed, 90);
1357 int target_row = -1;
1358 for (
int i = 0; i < static_cast<int>(rows.size()); ++i) {
1359 if (rows[i].display_id == current_id &&
1369 ImGuiListClipper clipper;
1370 clipper.Begin(
static_cast<int>(rows.size()));
1372 clipper.IncludeItemByIndex(target_row);
1374 while (clipper.Step()) {
1375 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
1377 const bool selected = row == target_row;
1378 ImGui::TableNextRow();
1382 ImGui::PushID(message.
expanded ?
"expanded" :
"vanilla");
1385 std::snprintf(id_label,
sizeof(id_label),
"$%04X",
1387 if (ImGui::Selectable(id_label, selected,
1388 ImGuiSelectableFlags_SpanAllColumns |
1389 ImGuiSelectableFlags_AllowOverlap)) {
1394 }
else if (!selected) {
1400 address = message.
address]() {
1401 std::vector<gui::MenuItemSpec> items;
1402 auto open_item = gui::MenuItemSpec::Conditional(
1404 [this, display_id]() {
1405 FinalizePendingUndo();
1406 OpenMessageById(display_id);
1408 [
this]() { return current_parse_errors_.empty(); });
1410 open_item.separator_after =
true;
1411 items.push_back(std::move(open_item));
1414 if (display_id >= 0 &&
1424 ImGui::SetScrollHereY(0.25f);
1429 message.expanded ? tr(
"Expanded") : tr(
"Vanilla"),
1433 const std::string preview =
1435 ImGui::TextUnformatted(preview.c_str());
1438 ImGui::TextUnformatted(
util::HexLong(message.address).c_str());
1446void MessageEditor::DrawMessageStructure() {
1447 if (!ImGui::CollapsingHeader(tr(
"Message Structure"),
1448 ImGuiTreeNodeFlags_DefaultOpen)) {
1451 ImGui::Text(tr(
"Lines: %d"), message_preview_.text_line + 1);
1453 int scroll_count = 0;
1454 int current_line_chars = 0;
1458 for (
size_t i = 0; i < current_message_.Data.size(); i++) {
1459 const uint8_t
byte = current_message_.Data[i];
1463 tr(
" [V] Scroll at byte %zu (line %d, %d chars)"), i,
1464 line_num, current_line_chars);
1465 current_line_chars = 0;
1467 }
else if (
byte ==
kLine1) {
1468 ImGui::TextColored(line_color, tr(
" [1] Line 1 at byte %zu"), i);
1469 current_line_chars = 0;
1471 }
else if (
byte ==
kLine2) {
1472 ImGui::TextColored(line_color, tr(
" [2] Line 2 at byte %zu"), i);
1473 current_line_chars = 0;
1475 }
else if (
byte ==
kLine3) {
1476 ImGui::TextColored(line_color, tr(
" [3] Line 3 at byte %zu"), i);
1477 current_line_chars = 0;
1479 }
else if (
byte < 100) {
1480 current_line_chars++;
1484 if (scroll_count == 0) {
1485 ImGui::TextDisabled(tr(
"No scroll breaks in this message"));
1487 ImGui::Text(tr(
"Total scroll breaks: %d"), scroll_count);
1491 ImGui::TextDisabled(tr(
"Line width budget (max ~170px):"));
1492 const int estimated_line_width = current_line_chars * 8;
1493 const float width_ratio =
static_cast<float>(estimated_line_width) / 170.0f;
1497 ImGui::TextColored(width_color, tr(
"Last line: ~%dpx / 170px (%d chars)"),
1498 estimated_line_width, current_line_chars);
1501void MessageEditor::DrawCurrentMessage() {
1502 DrawMessageHeader();
1505 const float available = ImGui::GetContentRegionAvail().y;
1506 const float max_editor = std::max(96.0f, available - 160.0f);
1507 editor_pane_height_ = std::clamp(editor_pane_height_, 96.0f, max_editor);
1509 if (ImGui::BeginChild(
"##MessageTextPane", ImVec2(0, editor_pane_height_),
1511 DrawDialogueTextPane(ImGui::GetContentRegionAvail().y);
1515 ImGui::InvisibleButton(
"##message_editor_split", ImVec2(-1.0f, 6.0f));
1516 if (ImGui::IsItemHovered() || ImGui::IsItemActive()) {
1517 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
1519 if (ImGui::IsItemActive()) {
1520 editor_pane_height_ += ImGui::GetIO().MouseDelta.y;
1523 if (!current_parse_errors_.empty() || !current_parse_warnings_.empty()) {
1524 if (ImGui::BeginChild(
"##MessageDiagnostics", ImVec2(0, 72),
true)) {
1525 if (!current_parse_errors_.empty()) {
1527 for (
const auto& error : current_parse_errors_) {
1528 ImGui::BulletText(
"%s", error.c_str());
1531 if (!current_parse_warnings_.empty()) {
1533 tr(
"Message parse warnings"));
1534 for (
const auto& warning : current_parse_warnings_) {
1535 ImGui::BulletText(
"%s", warning.c_str());
1542 if (!line_warnings.empty()) {
1544 for (
const auto& warning : line_warnings) {
1545 ImGui::BulletText(
"%s", warning.c_str());
1551 if (ImGui::BeginChild(
"##MessagePreview", ImVec2(0, 0),
true)) {
1552 EnsureFontTexturesReady();
1553 Text(tr(
"Message Preview"));
1555 if (Button(tr(
"View Palette"))) {
1556 ImGui::OpenPopup(
"Palette");
1558 if (ImGui::BeginPopup(
"Palette")) {
1563 ImGui::TextDisabled(tr(
"Scroll the preview with the mouse wheel"));
1566 BeginChild(
"CurrentGfxFont", ImVec2(0, 200),
true,
1567 ImGuiWindowFlags_NoScrollWithMouse);
1568 current_font_gfx16_canvas_.GetConfig().role =
1570 current_font_gfx16_canvas_.DrawBackground();
1572 current_font_gfx16_canvas_.DrawContextMenu();
1574 if (ImGui::IsWindowHovered()) {
1575 const float wheel = ImGui::GetIO().MouseWheel;
1576 if (wheel > 0 && message_preview_.shown_lines > 0) {
1577 message_preview_.shown_lines--;
1578 }
else if (wheel < 0 && message_preview_.shown_lines <
1579 message_preview_.text_line - 2) {
1580 message_preview_.shown_lines++;
1584 const ImVec2 preview_canvas_size = current_font_gfx16_canvas_.canvas_size();
1585 const float dest_width = std::max(0.0f, preview_canvas_size.x - 8.0f);
1586 const float dest_height = std::max(0.0f, preview_canvas_size.y - 8.0f);
1587 float src_height = 0.0f;
1588 if (dest_width > 0.0f && dest_height > 0.0f) {
1589 const float src_width =
1591 src_height = std::min(dest_height * 0.5f,
1593 current_font_gfx16_canvas_.DrawBitmap(
1594 current_font_gfx16_bitmap_, ImVec2(0, 0),
1595 ImVec2(dest_width, dest_height),
1596 ImVec2(0, message_preview_.shown_lines * 16),
1597 ImVec2(src_width, src_height));
1601 ImDrawList* overlay_draw_list = ImGui::GetWindowDrawList();
1602 const ImVec2 canvas_p0 = current_font_gfx16_canvas_.zero_point();
1603 const ImVec2 canvas_sz = current_font_gfx16_canvas_.canvas_size();
1604 const float line_height = 16.0f;
1605 float scale_y = 1.0f;
1606 if (dest_height > 0.0f && src_height > 0.0f) {
1607 scale_y = dest_height / src_height;
1609 const ImU32 marker_color =
1611 for (
int marker_line : message_preview_.scroll_marker_lines) {
1613 (marker_line - message_preview_.shown_lines) * line_height;
1614 const float y = canvas_p0.y + src_y * scale_y;
1615 if (y >= canvas_p0.y && y <= canvas_p0.y + canvas_sz.y) {
1616 overlay_draw_list->AddLine(ImVec2(canvas_p0.x, y),
1617 ImVec2(canvas_p0.x + canvas_sz.x, y),
1618 marker_color, 1.5f);
1619 overlay_draw_list->AddText(
1620 ImVec2(canvas_p0.x + canvas_sz.x + 4, y - 6), marker_color,
1626 current_font_gfx16_canvas_.DrawGrid();
1627 current_font_gfx16_canvas_.DrawOverlay();
1629 DrawMessageStructure();
1634void MessageEditor::DrawFontAtlas() {
1637 EnsureFontTexturesReady();
1640 font_gfx_canvas_.DrawBitmap(font_gfx_bitmap_, 0, 0, 2.0f);
1641 font_gfx_canvas_.DrawTileSelector(16, 32);
1643 ImGui::TextDisabled(
1644 tr(
"2bpp dialogue font at $070000. Click a tile to inspect it."));
1647void MessageEditor::DrawExpandedMessageSettings() {
1652 if (HasMessageSourceLayout()) {
1655 tr(
"Expanded dialogue loads automatically from the project bundle. "
1656 "Reload it after resolving a source error. Inspecting the built ROM "
1658 "manifest data range and does not replace the canonical source."));
1659 if (ImGui::Button(tr(
"Load canonical source"))) {
1660 const auto status = LoadCanonicalExpandedSource();
1662 message_bundle_status_ = std::string(status.message());
1663 message_bundle_status_error_ =
true;
1668 if (ImGui::Button(HasMessageSourceLayout() ? tr(
"Inspect built ROM")
1669 : tr(
"Load from ROM"))) {
1670 auto status = LoadExpandedMessagesFromRom();
1672 LOG_WARN(
"MessageEditor",
"Load from ROM: %s",
1673 std::string(status.message()).c_str());
1674 message_bundle_status_ =
1675 absl::StrFormat(
"Load from ROM failed: %s", status.message());
1676 message_bundle_status_error_ =
true;
1680 if (ImGui::Button(tr(
"Load from File"))) {
1682 if (!path.empty()) {
1683 const auto status = LoadExpandedMessagesFromBin(path);
1685 message_bundle_status_ = std::string(status.message());
1686 message_bundle_status_error_ =
true;
1687 if (
auto* popup_manager = dependencies_.popup_manager) {
1688 popup_manager->Show(
"Error");
1694 if (expanded_messages_.empty()) {
1695 ImGui::TextDisabled(
1696 tr(
"No expanded bank loaded. Vanilla dialogue is still editable."));
1700 ImGui::Text(tr(
"Source: %s"), expanded_message_path_.c_str());
1701 ImGui::Text(tr(
"Messages: %lu"), expanded_messages_.size());
1703 const auto inspection_range = ExpandedInspectionRange();
1704 const int capacity = inspection_range.second - inspection_range.first + 1;
1705 const int used = CalculateExpandedBankUsage();
1706 const int remaining = capacity - used;
1707 const float usage_ratio =
1708 capacity > 0 ?
static_cast<float>(used) /
static_cast<float>(capacity)
1715 char capacity_label[64];
1716 std::snprintf(capacity_label,
sizeof(capacity_label),
"%d / %d bytes", used,
1718 ImGui::ProgressBar(std::clamp(usage_ratio, 0.0f, 1.0f), ImVec2(-1, 0),
1721 ImGui::TextDisabled(tr(
"%d bytes free"), remaining);
1723 if (ImGui::Button(tr(
"Add New Message"))) {
1725 new_message.
ID = expanded_messages_.back().ID + 1;
1727 expanded_messages_.back().Address +
1728 static_cast<int>(expanded_messages_.back().Data.size());
1729 expanded_messages_.push_back(new_message);
1730 MarkDomainDirty(SaveDomain::kExpandedMessages);
1731 const int display_id = expanded_message_base_id_ + new_message.
ID;
1732 if (display_id >= 0 &&
1733 static_cast<size_t>(display_id) >= parsed_messages_.size()) {
1734 parsed_messages_.resize(display_id + 1);
1736 if (current_parse_errors_.empty()) {
1737 FinalizePendingUndo();
1738 OpenMessageById(display_id);
1742 if (ImGui::Button(tr(
"Export to JSON"))) {
1744 if (!path.empty()) {
1749 DrawMessageSourceSync();
1752void MessageEditor::DrawTokenInserter(
const std::vector<TextElement>& elements,
1753 std::string& filter,
bool use_parameter) {
1754 ImGui::SetNextItemWidth(-1);
1755 ImGui::InputTextWithHint(
"##token_filter",
"Filter tokens", &filter);
1759 TableSetupColumn(
"Token", ImGuiTableColumnFlags_WidthFixed, 110);
1760 TableSetupColumn(
"Description", ImGuiTableColumnFlags_WidthStretch);
1762 for (
const auto& text_element : elements) {
1763 if (!filter.empty() &&
1764 !ContainsCaseInsensitive(text_element.GenericToken, filter) &&
1765 !ContainsCaseInsensitive(text_element.Description, filter)) {
1768 ImGui::TableNextRow();
1770 ImGui::PushID(text_element.ID);
1771 const std::string token =
1772 use_parameter ? text_element.GetParamToken(command_parameter_)
1773 : text_element.GenericToken;
1774 if (ImGui::Selectable(text_element.GenericToken.c_str(),
false,
1775 ImGuiSelectableFlags_SpanAllColumns)) {
1776 InsertTokenAtCursor(token);
1778 if (ImGui::IsItemHovered()) {
1779 ImGui::SetTooltip(
"Insert %s", token.c_str());
1783 TextWrapped(
"%s", text_element.Description.c_str());
1788void MessageEditor::DrawTextCommands() {
1790 if (ImGui::IsItemHovered()) {
1791 ImGui::SetTooltip(
"Argument byte for commands such as [W:02] or [SPD:00].");
1793 DrawTokenInserter(TextCommands, command_filter_,
true);
1796void MessageEditor::DrawSpecialCharacters() {
1797 DrawTokenInserter(SpecialChars, symbol_filter_,
false);
1800void MessageEditor::DrawDictionary() {
1801 ImGui::SetNextItemWidth(-1);
1802 ImGui::InputTextWithHint(
"##dictionary_filter",
"Filter dictionary",
1803 &dictionary_filter_);
1804 std::vector<int> visible;
1805 visible.reserve(message_preview_.all_dictionaries_.size());
1807 i < static_cast<int>(message_preview_.all_dictionaries_.size()); ++i) {
1808 const auto& dictionary = message_preview_.all_dictionaries_[i];
1809 if (!dictionary_filter_.empty() &&
1810 !ContainsCaseInsensitive(dictionary.Contents, dictionary_filter_) &&
1811 !ContainsCaseInsensitive(dictionary.Token, dictionary_filter_)) {
1814 visible.push_back(i);
1816 if (visible.empty()) {
1817 ImGui::TextDisabled(tr(
"No dictionary entries match."));
1820 if (!BeginTable(
"##Dictionary", 3,
1824 TableSetupColumn(
"Token", ImGuiTableColumnFlags_WidthFixed, 72);
1825 TableSetupColumn(
"Text", ImGuiTableColumnFlags_WidthStretch);
1826 TableSetupColumn(
"Len", ImGuiTableColumnFlags_WidthFixed, 40);
1829 ImGuiListClipper clipper;
1830 clipper.Begin(
static_cast<int>(visible.size()));
1831 while (clipper.Step()) {
1832 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
1833 const auto& dictionary = message_preview_.all_dictionaries_[visible[row]];
1834 ImGui::TableNextRow();
1836 ImGui::PushID(dictionary.ID);
1837 if (ImGui::Selectable(dictionary.Token.c_str(),
false,
1838 ImGuiSelectableFlags_SpanAllColumns)) {
1839 InsertTokenAtCursor(dictionary.Token);
1841 if (ImGui::IsItemHovered()) {
1842 ImGui::SetTooltip(
"Insert %s", dictionary.Token.c_str());
1846 Text(
"%s", dictionary.Contents.c_str());
1848 Text(
"%d", dictionary.Length);
1854void MessageEditor::DrawMessageTools() {
1855 if (!ImGui::BeginTabBar(
"##MessageTools")) {
1858 if (ImGui::BeginTabItem(tr(
"Commands"))) {
1860 ImGui::EndTabItem();
1862 if (ImGui::BeginTabItem(tr(
"Symbols"))) {
1863 DrawSpecialCharacters();
1864 ImGui::EndTabItem();
1866 if (ImGui::BeginTabItem(tr(
"Dictionary"))) {
1868 ImGui::EndTabItem();
1872void MessageEditor::UpdateCurrentMessageFromText(
const std::string& text) {
1875 MarkDomainDirty(current_message_is_expanded_ ? SaveDomain::kExpandedMessages
1876 : SaveDomain::kVanillaMessages);
1879 current_parse_errors_ = parse_result.errors;
1880 current_parse_warnings_ = parse_result.warnings;
1881 if (!parse_result.ok()) {
1885 std::string raw_text = text;
1886 raw_text.erase(std::remove(raw_text.begin(), raw_text.end(),
'\n'),
1889 current_message_.RawString = raw_text;
1890 current_message_.ContentsParsed = text;
1891 current_message_.Data = std::move(parse_result.bytes);
1893 int parsed_index = current_message_index_;
1894 if (current_message_is_expanded_) {
1895 parsed_index = expanded_message_base_id_ + current_message_index_;
1898 if (parsed_index >= 0) {
1899 if (
static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
1900 parsed_messages_.resize(parsed_index + 1);
1902 parsed_messages_[parsed_index] = text;
1905 if (current_message_is_expanded_) {
1906 if (current_message_index_ >= 0 &&
1907 current_message_index_ <
static_cast<int>(expanded_messages_.size())) {
1908 expanded_messages_[current_message_index_] = current_message_;
1911 if (current_message_index_ >= 0 &&
1912 current_message_index_ <
static_cast<int>(list_of_texts_.size())) {
1913 list_of_texts_[current_message_index_] = current_message_;
1917 DrawMessagePreview();
1920void MessageEditor::ImportMessageBundleFromFile(
const std::string& path) {
1921 message_bundle_status_.clear();
1922 message_bundle_status_error_ =
false;
1925 if (!entries_or.ok()) {
1926 message_bundle_status_ =
1927 absl::StrFormat(
"Import failed: %s", entries_or.status().message());
1928 message_bundle_status_error_ =
true;
1933 const bool rom_dirty_before = rom_ && rom_->dirty();
1934 const bool importing_canonical = IsCanonicalBundlePath(path);
1935 std::string canonical_sha;
1936 if (importing_canonical) {
1937 const auto sha_or = HashFileSha256(std::filesystem::path(path));
1939 message_bundle_status_ = std::string(sha_or.status().message());
1940 message_bundle_status_error_ =
true;
1943 canonical_sha = sha_or.value();
1949 int duplicate_errors = 0;
1950 int parse_error_entries = 0;
1951 int vanilla_updated = 0;
1952 int expanded_updated = 0;
1953 int expanded_created = 0;
1954 bool expanded_modified =
false;
1955 std::vector<std::string> issue_samples;
1957 auto add_issue_sample = [&issue_samples](
const std::string& issue) {
1958 constexpr size_t kMaxIssueSamples = 4;
1959 if (issue_samples.size() < kMaxIssueSamples) {
1960 issue_samples.push_back(issue);
1968 std::unordered_map<std::string, int> seen_entries;
1970 auto entries = entries_or.value();
1971 for (
const auto& entry : entries) {
1972 const std::string entry_key = make_entry_key(entry);
1973 if (seen_entries.find(entry_key) != seen_entries.end()) {
1976 add_issue_sample(absl::StrFormat(
"Duplicate entry for %s", entry_key));
1979 seen_entries.emplace(entry_key, 1);
1983 warnings +=
static_cast<int>(parse_result.warnings.size());
1984 warnings +=
static_cast<int>(line_warnings.size());
1986 if (!parse_result.ok()) {
1988 parse_error_entries++;
1989 if (!parse_result.errors.empty()) {
1990 add_issue_sample(absl::StrFormat(
"Parse error for %s: %s", entry_key,
1991 parse_result.errors.front()));
1993 add_issue_sample(absl::StrFormat(
"Parse error for %s", entry_key));
1998 if (entry.bank == MessageBank::kVanilla) {
1999 if (entry.id < 0 || entry.id >=
static_cast<int>(list_of_texts_.size())) {
2002 absl::StrFormat(
"Vanilla ID out of range: %d", entry.id));
2005 auto& message = list_of_texts_[entry.id];
2006 message.RawString = entry.text;
2007 message.ContentsParsed = entry.text;
2008 message.Data = parse_result.bytes;
2009 message.DataParsed = parse_result.bytes;
2010 if (entry.id >= 0 &&
2011 entry.id <
static_cast<int>(parsed_messages_.size())) {
2012 parsed_messages_[entry.id] = entry.text;
2020 absl::StrFormat(
"Expanded ID out of range: %d", entry.id));
2023 if (entry.id >=
static_cast<int>(expanded_messages_.size())) {
2024 const int old_size =
static_cast<int>(expanded_messages_.size());
2025 const int target_size = entry.id + 1;
2026 expanded_messages_.resize(target_size);
2027 for (
int i = old_size; i < target_size; ++i) {
2028 expanded_messages_[i].ID = i;
2030 expanded_created += target_size - old_size;
2032 auto& message = expanded_messages_[entry.id];
2033 message.RawString = entry.text;
2034 message.ContentsParsed = entry.text;
2035 message.Data = parse_result.bytes;
2036 message.DataParsed = parse_result.bytes;
2037 const int parsed_index = expanded_message_base_id_ + entry.id;
2038 if (parsed_index >= 0) {
2039 if (
static_cast<size_t>(parsed_index) >= parsed_messages_.size()) {
2040 parsed_messages_.resize(parsed_index + 1);
2042 parsed_messages_[parsed_index] = entry.text;
2044 expanded_modified =
true;
2050 if (expanded_modified) {
2051 int pos = ExpandedInspectionRange().first;
2052 for (
auto& message : expanded_messages_) {
2053 message.Address = pos;
2054 pos +=
static_cast<int>(message.Data.size()) + 1;
2059 message_bundle_status_ = absl::StrFormat(
2060 "Import finished with %d errors (%d applied: vanilla %d updated, "
2061 "expanded %d updated/%d created; %d warnings, %d duplicates, %d "
2063 errors, applied, vanilla_updated, expanded_updated, expanded_created,
2064 warnings, duplicate_errors, parse_error_entries);
2065 if (!issue_samples.empty()) {
2066 message_bundle_status_ = absl::StrFormat(
2067 "%s Example: %s", message_bundle_status_, issue_samples.front());
2069 message_bundle_status_error_ =
true;
2071 message_bundle_status_ = absl::StrFormat(
2072 "Imported %d messages (vanilla %d updated, expanded %d updated/%d "
2073 "created, %d warnings).",
2074 applied, vanilla_updated, expanded_updated, expanded_created, warnings);
2077 RestoreExpandedDraft(snapshot);
2079 rom_->set_dirty(rom_dirty_before);
2083 int current_display_id = current_message_index_;
2084 if (current_message_is_expanded_) {
2085 current_display_id = expanded_message_base_id_ + current_message_index_;
2087 if (current_display_id >= 0) {
2088 OpenMessageById(current_display_id);
2090 if (vanilla_updated > 0) {
2091 MarkDomainDirty(SaveDomain::kVanillaMessages);
2093 if (expanded_modified) {
2094 if (importing_canonical) {
2095 source_loaded_canonical_sha_ = canonical_sha;
2096 dirty_state_.expanded_messages =
false;
2097 InvalidateSourcePreview();
2098 expanded_message_path_ = path;
2100 source_loaded_canonical_sha_.clear();
2101 MarkDomainDirty(SaveDomain::kExpandedMessages);
2106void MessageEditor::DrawMessagePreview() {
2108 message_preview_.DrawMessagePreview(current_message_);
2111 if (message_preview_.current_preview_data_.empty()) {
2112 LOG_WARN(
"MessageEditor",
"Preview data is empty, skipping bitmap update");
2116 if (current_font_gfx16_bitmap_.is_active()) {
2119 current_font_gfx16_bitmap_.set_data(message_preview_.current_preview_data_);
2121 if (current_font_gfx16_bitmap_.palette().empty() &&
2122 !font_preview_colors_.empty()) {
2123 current_font_gfx16_bitmap_.SetPalette(font_preview_colors_);
2127 if (!current_font_gfx16_bitmap_.surface()) {
2128 LOG_ERROR(
"MessageEditor",
"Bitmap surface is null after set_data()");
2133 const auto command = current_font_gfx16_bitmap_.texture()
2140 "Updated message preview bitmap (size: %zu) and queued texture update",
2141 message_preview_.current_preview_data_.size());
2146 message_preview_.current_preview_data_);
2147 current_font_gfx16_bitmap_.SetPalette(font_preview_colors_);
2152 "Created message preview bitmap (%dx%d) with 8-bit depth and "
2153 "queued texture creation",
2158absl::Status MessageEditor::Save() {
2159 if (!rom_ || !rom_->is_loaded()) {
2160 return absl::FailedPreconditionError(
"ROM not loaded");
2162 if (!current_parse_errors_.empty()) {
2163 return absl::FailedPreconditionError(absl::StrFormat(
2164 "Current message has parse errors: %s", current_parse_errors_.front()));
2167 return SaveDirtyDomains(
true,
2172absl::StatusOr<MessageEditor::SavePlan> MessageEditor::BuildSavePlan(
2173 bool include_font_widths,
bool include_vanilla_messages,
2174 bool include_expanded_messages)
const {
2175 if (!rom_ || !rom_->is_loaded()) {
2176 return absl::FailedPreconditionError(
"ROM not loaded");
2181 if (include_font_widths && dirty_state_.font_widths) {
2185 std::vector<uint8_t>(message_preview_.width_array.begin(),
2186 message_preview_.width_array.end())});
2189 if (include_vanilla_messages && dirty_state_.vanilla_messages) {
2190 std::optional<size_t> expected_message_count;
2191 if (dependencies_.project &&
2192 dependencies_.project->hack_manifest.loaded()) {
2193 const int manifest_count =
2194 dependencies_.project->hack_manifest.message_layout().vanilla_count;
2195 if (manifest_count > 0) {
2196 expected_message_count =
static_cast<size_t>(manifest_count);
2204 for (
const auto& write : vanilla_plan.
writes()) {
2206 {SaveDomain::kVanillaMessages, write.start(), write.bytes()});
2210 if (include_expanded_messages && dirty_state_.expanded_messages &&
2211 !expanded_messages_.empty()) {
2212 const auto range = ExpandedInspectionRange();
2213 const int start = range.first;
2214 const int end = range.second;
2215 if (start < 0 || end < start) {
2216 return absl::InvalidArgumentError(
"Invalid expanded message region");
2218 if (
static_cast<uint64_t
>(start) >= rom_->size() ||
2219 static_cast<uint64_t
>(end) >= rom_->size()) {
2220 return absl::OutOfRangeError(
2221 "Expanded message region is outside the ROM");
2224 const int64_t capacity =
static_cast<int64_t
>(end) - start + 1;
2225 std::vector<uint8_t> bytes;
2226 bytes.reserve(
static_cast<size_t>(capacity));
2229 for (
size_t index = 0; index < expanded_messages_.size(); ++index) {
2230 const auto& message = expanded_messages_[index];
2233 return absl::InvalidArgumentError(
2234 absl::StrFormat(
"Expanded message %d is invalid: %s",
2235 static_cast<int>(index), parsed.errors.front()));
2237 if (message.RawString.find(
"[BANK]") != std::string::npos) {
2238 return absl::InvalidArgumentError(absl::StrFormat(
2239 "Expanded message %d contains [BANK], which is only valid in the "
2240 "vanilla message stream",
2241 static_cast<int>(index)));
2244 const int64_t needed =
static_cast<int64_t
>(parsed.bytes.size()) + 1;
2245 if (
static_cast<int64_t
>(bytes.size()) + needed + 1 > capacity) {
2246 return absl::ResourceExhaustedError(absl::StrFormat(
2247 "Expanded message data exceeds bank boundary "
2248 "(at message %d, used=%d, needed=%d, capacity=%d, end=0x%06X)",
2249 static_cast<int>(index),
static_cast<int>(bytes.size()),
2250 static_cast<int>(needed),
static_cast<int>(capacity), end));
2254 static_cast<int>(bytes.size()));
2255 bytes.insert(bytes.end(), parsed.bytes.begin(), parsed.bytes.end());
2258 bytes.push_back(0xFF);
2261 plan.
writes.push_back({SaveDomain::kExpandedMessages,
2262 static_cast<uint32_t
>(start), std::move(bytes)});
2265 for (
const auto& write : plan.
writes) {
2266 const uint64_t end =
2267 static_cast<uint64_t
>(write.start) + write.bytes.size();
2268 if (end > rom_->size() || end > std::numeric_limits<uint32_t>::max()) {
2269 return absl::OutOfRangeError(absl::StrFormat(
2270 "Message save range [0x%06X, 0x%06llX) is outside the ROM",
2271 write.start,
static_cast<unsigned long long>(end)));
2278absl::Status MessageEditor::ValidateSavePlan(
const SavePlan& plan)
const {
2279 if (!dependencies_.project ||
2280 !dependencies_.project->hack_manifest.loaded() || plan.
writes.empty()) {
2281 return absl::OkStatus();
2284 std::vector<std::pair<uint32_t, uint32_t>> ranges;
2285 ranges.reserve(plan.
writes.size());
2286 for (
const auto& write : plan.
writes) {
2287 ranges.emplace_back(write.start, write.end());
2290 const auto& yaze_project = *dependencies_.project;
2291 if (yaze_project.rom_metadata.write_policy ==
2294 absl::StrContains(yaze_project.hack_manifest.hack_name(),
2295 "Oracle of Secrets")) {
2300 yaze_project.hack_manifest, yaze_project.rom_metadata.write_policy,
2301 ranges,
"message data",
"MessageEditor", dependencies_.toast_manager);
2305absl::Status MessageEditor::ApplySavePlan(
const SavePlan& plan) {
2306 if (plan.
writes.empty()) {
2307 return absl::OkStatus();
2311 for (
const auto& write : plan.
writes) {
2312 const char* label =
"MessageData";
2313 switch (write.domain) {
2314 case SaveDomain::kFontWidths:
2315 label =
"MessageFontWidths";
2317 case SaveDomain::kVanillaMessages:
2318 label =
"VanillaMessageBank";
2320 case SaveDomain::kExpandedMessages:
2321 label =
"ExpandedMessageBank";
2329 for (
const auto& write : plan.
writes) {
2331 rom_->WriteVector(
static_cast<int>(write.start), write.bytes));
2336 for (
size_t index = 0; index < expanded_messages_.size() &&
2339 expanded_messages_[index].Address =
2342 SyncCurrentExpandedMessageAddress();
2345 return absl::OkStatus();
2348absl::Status MessageEditor::SaveDirtyDomains(
bool include_font_widths,
2349 bool include_vanilla_messages,
2350 bool include_expanded_messages) {
2352 BuildSavePlan(include_font_widths, include_vanilla_messages,
2353 include_expanded_messages));
2356 RecordSavedDomains(plan);
2357 return absl::OkStatus();
2360absl::Status MessageEditor::SaveExpandedMessages() {
2361 return SaveDirtyDomains(
false,
2366absl::Status MessageEditor::BeginSaveTransaction() {
2367 if (save_transaction_active_) {
2368 return absl::FailedPreconditionError(
2369 "Message save transaction is already active");
2371 transaction_dirty_snapshot_ = dirty_state_;
2372 transaction_saved_domains_ = {};
2373 transaction_expanded_address_snapshot_.clear();
2374 transaction_expanded_address_snapshot_.reserve(expanded_messages_.size());
2375 for (
const auto& message : expanded_messages_) {
2376 transaction_expanded_address_snapshot_.push_back(message.Address);
2378 save_transaction_active_ =
true;
2379 return absl::OkStatus();
2382void MessageEditor::RollbackSaveTransaction() {
2383 if (!save_transaction_active_) {
2386 dirty_state_ = transaction_dirty_snapshot_;
2387 for (
size_t index = 0; index < expanded_messages_.size() &&
2388 index < transaction_expanded_address_snapshot_.size();
2390 expanded_messages_[index].Address =
2391 transaction_expanded_address_snapshot_[index];
2393 SyncCurrentExpandedMessageAddress();
2394 transaction_dirty_snapshot_ = {};
2395 transaction_saved_domains_ = {};
2396 transaction_expanded_address_snapshot_.clear();
2397 save_transaction_active_ =
false;
2400void MessageEditor::CommitSaveTransaction() {
2401 if (!save_transaction_active_) {
2404 ClearSavedDomains(transaction_saved_domains_);
2405 transaction_dirty_snapshot_ = {};
2406 transaction_saved_domains_ = {};
2407 transaction_expanded_address_snapshot_.clear();
2408 save_transaction_active_ =
false;
2412 bool* dirty =
nullptr;
2413 bool* transaction_saved =
nullptr;
2415 case SaveDomain::kFontWidths:
2416 dirty = &dirty_state_.font_widths;
2417 transaction_saved = &transaction_saved_domains_.font_widths;
2419 case SaveDomain::kVanillaMessages:
2420 dirty = &dirty_state_.vanilla_messages;
2421 transaction_saved = &transaction_saved_domains_.vanilla_messages;
2423 case SaveDomain::kExpandedMessages:
2424 dirty = &dirty_state_.expanded_messages;
2425 transaction_saved = &transaction_saved_domains_.expanded_messages;
2429 if (save_transaction_active_) {
2430 *transaction_saved =
false;
2432 if (domain == SaveDomain::kExpandedMessages) {
2433 InvalidateSourcePreview();
2435 if (rom_ && !(domain == SaveDomain::kExpandedMessages &&
2436 ExpandedEditsAreSourceOwned())) {
2437 rom_->set_dirty(
true);
2441void MessageEditor::RecordSavedDomains(
const SavePlan& plan) {
2444 if (!save_transaction_active_) {
2445 ClearSavedDomains(saved);
2448 transaction_saved_domains_.font_widths |= saved.font_widths;
2449 transaction_saved_domains_.vanilla_messages |= saved.vanilla_messages;
2450 transaction_saved_domains_.expanded_messages |= saved.expanded_messages;
2455 dirty_state_.font_widths =
false;
2458 dirty_state_.vanilla_messages =
false;
2461 dirty_state_.expanded_messages =
false;
2465void MessageEditor::SyncCurrentExpandedMessageAddress() {
2466 if (!current_message_is_expanded_ || current_message_index_ < 0 ||
2467 current_message_index_ >=
static_cast<int>(expanded_messages_.size())) {
2470 current_message_.Address = expanded_messages_[current_message_index_].Address;
2473absl::Status MessageEditor::LoadCanonicalExpandedSource() {
2474 if (!HasMessageSourceLayout()) {
2475 return absl::FailedPreconditionError(
2476 "The project hack manifest does not define messages.source");
2478 if (dirty_state_.expanded_messages || dirty_state_.vanilla_messages ||
2479 dirty_state_.font_widths || pending_undo_before_.has_value() ||
2480 !current_parse_errors_.empty()) {
2481 return absl::FailedPreconditionError(
2482 "The Message editor has pending edits; canonical source was not "
2486 if (!source_or.ok()) {
2487 return source_or.status();
2491 expanded_message_base_id_ = source_or->first_expanded_id;
2492 expanded_messages_ = std::move(source_or->messages);
2493 expanded_message_path_ = source_or->canonical_bundle_path.string();
2494 source_loaded_canonical_sha_ = std::move(source_or->source_sha256);
2495 parsed_messages_.resize(expanded_message_base_id_ +
2496 expanded_messages_.size());
2497 for (
const auto& message : expanded_messages_) {
2498 parsed_messages_[expanded_message_base_id_ + message.ID] =
2501 InvalidateSourcePreview();
2502 NoteLoadedMessages();
2503 message_bundle_status_ =
2504 absl::StrFormat(
"Loaded %zu expanded messages from project source: %s",
2505 expanded_messages_.size(), expanded_message_path_);
2506 message_bundle_status_error_ =
false;
2507 if (current_message_is_expanded_) {
2508 OpenMessageById(expanded_message_base_id_ + current_message_index_);
2510 return absl::OkStatus();
2513absl::Status MessageEditor::LoadExpandedMessagesFromBin(
2514 const std::string& path) {
2516 const bool rom_dirty_before = rom_ && rom_->dirty();
2517 expanded_message_base_id_ = ResolveExpandedMessageBaseId();
2518 parsed_messages_.resize(
2519 static_cast<size_t>(std::max(expanded_message_base_id_, 0)));
2520 expanded_messages_.clear();
2521 std::vector<std::string> parsed_expanded;
2522 std::string mutable_path = path;
2525 message_preview_.all_dictionaries_);
2526 if (!status.ok() || expanded_messages_.empty()) {
2527 RestoreExpandedDraft(snapshot);
2529 rom_->set_dirty(rom_dirty_before);
2534 return absl::NotFoundError(
"No expanded messages found in file");
2536 parsed_messages_.insert(parsed_messages_.end(), parsed_expanded.begin(),
2537 parsed_expanded.end());
2538 expanded_message_path_ = path;
2539 source_loaded_canonical_sha_.clear();
2540 NoteLoadedMessages();
2541 MarkDomainDirty(SaveDomain::kExpandedMessages);
2542 return absl::OkStatus();
2545absl::Status MessageEditor::LoadExpandedMessagesFromRom() {
2546 if (!rom_ || !rom_->is_loaded()) {
2547 return absl::FailedPreconditionError(
"ROM not loaded");
2551 const bool rom_dirty_before = rom_ && rom_->dirty();
2552 const auto range = ExpandedInspectionRange();
2553 const int end = std::min(range.second,
static_cast<int>(rom_->size()) - 1);
2555 if (loaded.empty()) {
2556 RestoreExpandedDraft(snapshot);
2558 rom_->set_dirty(rom_dirty_before);
2560 return absl::NotFoundError(
2561 "No expanded messages found in ROM at the manifest data range");
2564 expanded_message_base_id_ = ResolveExpandedMessageBaseId();
2565 parsed_messages_.resize(
2566 static_cast<size_t>(std::max(expanded_message_base_id_, 0)));
2567 expanded_messages_ = std::move(loaded);
2568 auto parsed_expanded =
2570 for (
const auto& msg : expanded_messages_) {
2571 if (msg.ID >= 0 && msg.ID <
static_cast<int>(parsed_expanded.size())) {
2572 parsed_messages_.push_back(parsed_expanded[msg.ID]);
2576 expanded_message_path_ =
"(ROM inspection)";
2577 source_loaded_canonical_sha_.clear();
2578 dirty_state_.expanded_messages =
false;
2579 InvalidateSourcePreview();
2580 NoteLoadedMessages();
2581 return absl::OkStatus();
2584int MessageEditor::CalculateExpandedBankUsage()
const {
2585 if (expanded_messages_.empty())
2588 for (
const auto& msg : expanded_messages_) {
2589 total +=
static_cast<int>(msg.Data.size()) + 1;
2595absl::Status MessageEditor::Cut() {
2597 if (!message_text_box_.text.empty()) {
2599 message_text_box_.Cut();
2601 return absl::OkStatus();
2604absl::Status MessageEditor::Paste() {
2606 if (ImGui::GetClipboardText() !=
nullptr) {
2608 message_text_box_.Paste();
2610 return absl::OkStatus();
2613absl::Status MessageEditor::Copy() {
2615 if (message_text_box_.selection_length > 0) {
2617 message_text_box_.Copy();
2619 return absl::OkStatus();
2622void MessageEditor::PushUndoSnapshot() {
2623 if (pending_undo_before_.has_value()) {
2626 if (pending_undo_before_->message_index == current_message_index_ &&
2627 pending_undo_before_->is_expanded == current_message_is_expanded_) {
2630 FinalizePendingUndo();
2634 int parsed_index = current_message_index_;
2635 if (current_message_is_expanded_) {
2636 parsed_index = expanded_message_base_id_ + current_message_index_;
2639 if (parsed_index >= 0 &&
2640 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2641 text = parsed_messages_[parsed_index];
2643 pending_undo_before_ =
2645 current_message_is_expanded_};
2648void MessageEditor::FinalizePendingUndo() {
2649 if (!pending_undo_before_.has_value())
2654 const int message_index = pending_undo_before_->
message_index;
2655 const bool is_expanded = pending_undo_before_->is_expanded;
2659 if (message_index < 0 ||
2660 message_index >=
static_cast<int>(expanded_messages_.size())) {
2661 pending_undo_before_.reset();
2664 after_message = expanded_messages_[message_index];
2666 if (message_index < 0 ||
2667 message_index >=
static_cast<int>(list_of_texts_.size())) {
2668 pending_undo_before_.reset();
2671 after_message = list_of_texts_[message_index];
2674 int parsed_index = message_index;
2676 parsed_index = expanded_message_base_id_ + message_index;
2679 if (parsed_index >= 0 &&
2680 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2681 text = parsed_messages_[parsed_index];
2684 message_index, is_expanded};
2686 undo_manager_.Push(std::make_unique<MessageEditAction>(
2687 std::move(*pending_undo_before_), std::move(after),
2689 pending_undo_before_.reset();
2693 current_message_ = snapshot.
message;
2695 current_message_is_expanded_ = snapshot.
is_expanded;
2697 const auto diagnostics =
2699 current_parse_errors_ = diagnostics.errors;
2700 current_parse_warnings_ = diagnostics.warnings;
2704 parsed_index = expanded_message_base_id_ + snapshot.
message_index;
2706 if (parsed_index >= 0 &&
2707 parsed_index <
static_cast<int>(parsed_messages_.size())) {
2708 parsed_messages_[parsed_index] = snapshot.
parsed_text;
2713 snapshot.
message_index <
static_cast<int>(expanded_messages_.size())) {
2718 snapshot.
message_index <
static_cast<int>(list_of_texts_.size())) {
2723 MarkDomainDirty(snapshot.
is_expanded ? SaveDomain::kExpandedMessages
2724 : SaveDomain::kVanillaMessages);
2725 DrawMessagePreview();
2728absl::Status MessageEditor::Undo() {
2729 FinalizePendingUndo();
2730 return undo_manager_.Undo();
2733absl::Status MessageEditor::Redo() {
2734 return undo_manager_.Redo();
2737void MessageEditor::Delete() {
2739 if (message_text_box_.selection_length == 0) {
2741 message_text_box_.clear();
2745void MessageEditor::SelectAll() {
2747 if (message_text_box_.selection_length == 0) {
2749 message_text_box_.SelectAll();
2752 message_text_box_.Focus();
2756absl::Status MessageEditor::Find() {
2759 find_panel_open_ =
true;
2760 find_focus_search_ =
true;
2761 if (dependencies_.window_manager) {
2762 dependencies_.window_manager->OpenWindow(dependencies_.session_id,
2763 "message.message_editor");
2765 return absl::OkStatus();
2768int MessageEditor::ReplaceCurrentMatch() {
2769 if (search_text_.empty()) {
2773 std::string& text = message_text_box_.text;
2774 const size_t cursor =
static_cast<size_t>(
2775 std::clamp(editor_cursor_, 0,
static_cast<int>(text.size())));
2776 const size_t from = find_has_match_ ? find_match_start_ : cursor;
2779 if (!match && from > 0) {
2787 text.replace(*match, search_text_.size(), replace_text_);
2788 pending_selection_start_ =
static_cast<int>(*match);
2789 pending_selection_end_ =
2790 pending_selection_start_ +
static_cast<int>(replace_text_.size());
2791 editor_cursor_ = pending_selection_end_;
2792 find_has_match_ =
false;
2793 ImGui::ClearActiveID();
2794 UpdateCurrentMessageFromText(text);
2795 FinalizePendingUndo();
2799int MessageEditor::ReplaceAllMatches() {
2800 if (search_text_.empty()) {
2803 if (!current_parse_errors_.empty()) {
2805 "Replace All blocked: resolve the current message parse errors first";
2806 replace_status_error_ =
true;
2810 auto replace_in_text = [&](std::string& text) ->
int {
2812 std::string search = search_text_;
2814 if (!case_sensitive_) {
2815 std::transform(search.begin(), search.end(), search.begin(), ::tolower);
2819 while (pos < text.size()) {
2820 std::string source = text;
2821 if (!case_sensitive_) {
2822 std::transform(source.begin(), source.end(), source.begin(), ::tolower);
2825 size_t found = source.find(search, pos);
2826 if (found == std::string::npos)
2829 if (match_whole_word_) {
2830 bool start_ok = (found == 0 || !std::isalnum(source[found - 1]));
2831 bool end_ok = (found + search.size() >= source.size() ||
2832 !std::isalnum(source[found + search.size()]));
2833 if (!start_ok || !end_ok) {
2839 text.replace(found, search_text_.size(), replace_text_);
2840 pos = found + replace_text_.size();
2846 struct PlannedReplacement {
2852 std::vector<PlannedReplacement> plan;
2854 const auto plan_replacements = [&](
const std::vector<MessageData>& messages,
2855 bool is_expanded) ->
bool {
2856 for (
size_t i = 0; i < messages.size(); ++i) {
2857 const int parsed_index =
2858 (is_expanded ? expanded_message_base_id_ : 0) +
static_cast<int>(i);
2859 if (parsed_index < 0 ||
2860 parsed_index >=
static_cast<int>(parsed_messages_.size())) {
2864 std::string text = parsed_messages_[parsed_index];
2865 const int count = replace_in_text(text);
2872 (is_expanded && text.find(
"[BANK]") != std::string::npos)) {
2873 const std::string error =
2874 !parsed.ok() ? parsed.errors.front()
2875 :
"[BANK] is not valid in expanded messages";
2876 replace_status_ = absl::StrFormat(
2877 "Replace All aborted at %s message %d: %s",
2878 is_expanded ?
"expanded" :
"vanilla",
static_cast<int>(i), error);
2879 replace_status_error_ =
true;
2884 {
static_cast<int>(i), is_expanded, std::move(text), count});
2890 if (!plan_replacements(list_of_texts_,
false) ||
2891 !plan_replacements(expanded_messages_,
true)) {
2895 const int previous_index = current_message_index_;
2896 const bool previous_expanded = current_message_is_expanded_;
2897 int total_replacements = 0;
2899 for (
const auto& replacement : plan) {
2900 current_message_ = replacement.is_expanded
2901 ? expanded_messages_[replacement.message_index]
2902 : list_of_texts_[replacement.message_index];
2903 current_message_index_ = replacement.message_index;
2904 current_message_is_expanded_ = replacement.is_expanded;
2905 message_text_box_.text = replacement.text;
2906 UpdateCurrentMessageFromText(replacement.text);
2907 FinalizePendingUndo();
2908 total_replacements += replacement.count;
2911 current_message_index_ = previous_index;
2912 current_message_is_expanded_ = previous_expanded;
2915 int current_parsed_idx = current_message_index_;
2916 if (current_message_is_expanded_) {
2917 current_parsed_idx = expanded_message_base_id_ + current_message_index_;
2919 if (current_parsed_idx >= 0 &&
2920 current_parsed_idx <
static_cast<int>(parsed_messages_.size())) {
2921 message_text_box_.text = parsed_messages_[current_parsed_idx];
2925 if (current_message_is_expanded_) {
2926 if (current_message_index_ >= 0 &&
2927 current_message_index_ <
static_cast<int>(expanded_messages_.size())) {
2928 current_message_ = expanded_messages_[current_message_index_];
2931 if (current_message_index_ >= 0 &&
2932 current_message_index_ <
static_cast<int>(list_of_texts_.size())) {
2933 current_message_ = list_of_texts_[current_message_index_];
2937 const auto current_diagnostics =
2939 current_parse_errors_ = current_diagnostics.errors;
2940 current_parse_warnings_ = current_diagnostics.warnings;
2942 return total_replacements;
const MessageLayout & message_layout() const
bool loaded() const
Check if the manifest has been loaded.
virtual void SetGameData(zelda3::GameData *game_data)
project::YazeProject * project() const
zelda3::GameData * game_data() const
EditorDependencies dependencies_
void DrawMessageSourceSync()
DirtyState transaction_dirty_snapshot_
MessageSourcePublishButtons DrawMessageSourcePublishButtons()
std::vector< std::string > baseline_messages_
bool source_sync_changed_
void RestoreExpandedDraft(const ExpandedDraftSnapshot &snapshot)
std::vector< std::string > parsed_messages_
std::string reference_view_text_
bool current_message_is_expanded_
bool replace_status_error_
std::string source_loaded_canonical_sha_
std::vector< MessageData > expanded_messages_
void ContributeStatus(StatusBar *status_bar) override
void ResolveFontPalette()
DirtyState transaction_saved_domains_
bool scroll_list_to_selection_
int pending_selection_start_
bool font_graphics_loaded_
bool ExpandedEditsAreSourceOwned() const
absl::Status Update() override
bool IsCanonicalBundlePath(const std::string &path) const
void FinalizePendingUndo()
absl::Status LoadExpandedMessagesFromRom()
void DrawDialogueTextPane(float height)
void DrawExpandedMessageSettings()
int pending_selection_end_
void UpdateCurrentMessageFromText(const std::string &text)
bool save_transaction_active_
std::vector< int > transaction_expanded_address_snapshot_
bool AdvanceFind(bool forward)
ExpandedDraftSnapshot CaptureExpandedDraft() const
void OptimizeCurrentMessage()
std::string source_sync_draft_sha_
bool HasUnpublishedMessageSourceChanges() const
bool HasMessageSourceLayout() const
absl::StatusOr< std::filesystem::path > ResolveCanonicalBundlePath() const
std::vector< std::string > current_parse_errors_
absl::Status Load() override
std::string MessageSourceIncomingPath() const
std::string replace_status_
absl::StatusOr< std::string > HashFileSha256(const std::filesystem::path &path) const
void HandleMessageShortcuts()
std::vector< std::string > current_parse_warnings_
std::string message_bundle_status_
std::pair< int, int > ExpandedInspectionRange() const
void Initialize() override
std::string replace_text_
void NoteLoadedMessages()
absl::Status LoadCanonicalExpandedSource()
void InsertTokenAtCursor(const std::string &token)
int CurrentDisplayId() const
void DrawMessagePreview()
std::string source_sync_summary_
std::vector< MessageRow > CollectMessageRows(bool apply_filter) const
bool ActivateFindMatch(const MessageRow &row, size_t offset, bool wrapped)
int expanded_message_base_id_
void PublishMessageSource()
bool message_bundle_status_error_
std::string_view ContentsForDisplayId(int display_id) const
void InvalidateSourcePreview()
std::vector< MessageData > list_of_texts_
std::string CurrentReferenceText() const
std::string source_sync_status_
bool OpenMessageById(int display_id)
gfx::SnesPalette font_preview_colors_
MessageData current_message_
void AcceptCurrentAsReference()
MessagePreview message_preview_
std::string source_sync_sha_before_
gui::TextBox message_text_box_
void DrawCurrentMessage()
bool project_source_load_attempted_
void SelectAdjacentMessage(int delta)
bool source_sync_status_error_
int ResolveExpandedMessageBaseId() const
void ImportMessageBundleFromFile(const std::string &path)
gfx::Bitmap current_font_gfx16_bitmap_
int ReplaceCurrentMatch()
void PreviewMessageSource()
int current_message_index_
std::string expanded_message_path_
void SetGameData(zelda3::GameData *game_data) override
A session-aware status bar displayed at the bottom of the application.
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
void SetEditorMode(const std::string &mode)
Set the current editor mode or tool.
const std::vector< VanillaMessageWrite > & writes() const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
SnesPalette * mutable_palette()
RAII timer for automatic timing management.
static void BeginToolbar(const char *label)
static bool ToolbarButton(const char *label, const ImVec2 &size=ImVec2(0, 0))
RAII guard for ImGui style colors.
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
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...
#define ICON_MD_NAVIGATE_BEFORE
#define ICON_MD_ARROW_FORWARD
#define ICON_MD_FONT_DOWNLOAD
#define ICON_MD_LIBRARY_ADD
#define ICON_MD_NAVIGATE_NEXT
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define PRINT_IF_ERROR(expression)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
bool MessageSurfaceFocused()
int MessageInputCallback(ImGuiInputTextCallbackData *data)
constexpr int kCharactersWidth
int GetExpandedTextDataStart()
constexpr uint8_t kScrollVertical
absl::StatusOr< MessageSourceSnapshot > LoadMessageSource(const project::YazeProject &project)
std::optional< ResolvedMessageId > ResolveMessageDisplayId(int display_id, int vanilla_count, int expanded_base_id, int expanded_count)
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)
absl::Status LoadExpandedMessages(std::string &expanded_message_path, std::vector< std::string > &parsed_messages, std::vector< MessageData > &expanded_messages, std::vector< DictionaryEntry > &dictionary)
std::string MessageListPreview(std::string_view text, size_t max_chars=96)
std::string MessageBankToString(MessageBank bank)
constexpr int kCurrentMessageWidth
constexpr int kCurrentMessageHeight
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
std::string ComputeMessageSourceSha256(std::string_view content)
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos, bool allow_bank_switch)
std::vector< std::string > ParseMessageData(std::vector< MessageData > &message_data, const std::vector< DictionaryEntry > &dictionary_entries)
constexpr uint8_t kMessageTerminator
std::vector< DictionaryEntry > BuildDictionaryEntries(Rom *rom)
absl::Status ExportMessagesToJson(const std::string &path, const std::vector< MessageData > &messages)
constexpr uint8_t kWidthArraySize
absl::Status ExportMessageBundleToJson(const std::string &path, const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
constexpr ImGuiTableFlags kMessageTableFlags
absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
constexpr char kOracleExpandedRomWriteRefusal[]
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
absl::StatusOr< MessageSourceSyncResult > SyncMessageSource(const project::YazeProject &project, const fs::path &incoming_bundle_path, const MessageSourceSyncOptions &options)
std::optional< size_t > FindNextTextMatch(std::string_view text, std::string_view needle, size_t start, bool case_sensitive, bool whole_word)
std::optional< size_t > FindPreviousTextMatch(std::string_view text, std::string_view needle, size_t before, bool case_sensitive, bool whole_word)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
bool MessageListQueryMatches(int display_id, uint32_t address, std::string_view contents, bool expanded, MessageBankFilter bank, std::string_view query)
void EndCanvas(Canvas &canvas)
std::string ColumnLabel(absl::string_view text, absl::string_view stable_id)
void BeginCanvas(Canvas &canvas, ImVec2 child_size)
void MemoryEditorPopup(const std::string &label, std::span< uint8_t > memory)
void SectionHeader(const char *icon, const char *label, const ImVec4 &color)
bool ItemContextMenu(const char *id, const std::function< std::vector< MenuItemSpec >()> &build, ImGuiPopupFlags flags)
Right-click menu for the last submitted ImGui item.
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded)
MenuItemSpec CopyToClipboardItem(const std::string &label, const std::string &text)
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
void StatusBadge(const char *text, ButtonType type)
std::string HexWord(uint16_t word, HexStringParams params)
std::string HexLong(uint32_t dword, HexStringParams params)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
std::optional< Source > source
project::YazeProject * project
WorkspaceWindowManager * window_manager
std::vector< uint8_t > Data
std::string OptimizeMessageForDictionary(std::string_view message_string, const std::vector< DictionaryEntry > &dictionary)
std::string loaded_canonical_sha
std::vector< MessageData > messages
std::vector< std::string > parsed_messages
std::string preview_summary
bool preview_status_error
std::string preview_draft_sha
std::string preview_status
std::vector< MessageData > vanilla_messages
std::string preview_sha_before
std::vector< int > expanded_message_addresses
bool saves_vanilla_messages
bool saves_expanded_messages
std::vector< PlannedWrite > writes
std::array< uint8_t, kWidthArraySize > width_array
std::vector< DictionaryEntry > all_dictionaries_
std::string expected_source_sha256
std::string source_sha256_before
std::string proposed_source_sha256
Shared empty / disabled panel presentation.
const char * action_label
std::function< void()> on_action
bool compact
Tighter vertical rhythm for drawers / inspectors.
core::HackManifest hack_manifest