yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
song_browser_view.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cstring>
6
7#include "absl/strings/str_format.h"
12#include "imgui/imgui.h"
13
14namespace yaze {
15namespace editor {
16namespace music {
17
22
24 // Search filter
25 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
26 ImGui::InputTextWithHint("##SongFilter", ICON_MD_SEARCH " Search songs...",
28
29 // Update filter cache if needed
30 if (std::string(search_buffer_) != last_search_buffer_ ||
34 }
35
36 // Bank Space Management Section
37 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " Bank Space")) {
38 ImGui::Indent(8.0f);
39
40 // Check for expanded music patch
41 if (bank.HasExpandedMusicPatch()) {
42 ImGui::TextColored(GetSuccessColor(), ICON_MD_CHECK_CIRCLE
43 " Oracle of Secrets expanded music detected");
44 const auto& info = bank.GetExpandedBankInfo();
45 ImGui::TextDisabled(tr("Expanded bank at $%06X, Aux at $%06X"),
46 info.main_rom_offset, info.aux_rom_offset);
47 ImGui::Spacing();
48 }
49
50 // Display space for each bank
51 static const char* bank_names[] = {"Overworld", "Dungeon", "Credits",
52 "Expanded", "Auxiliary"};
53 static const MusicBank::Bank banks[] = {
57
58 int num_banks = bank.HasExpandedMusicPatch() ? 5 : 3;
59
60 for (int i = 0; i < num_banks; ++i) {
61 auto space = bank.CalculateSpaceUsage(banks[i]);
62 if (space.total_bytes == 0)
63 continue; // Skip empty/invalid banks
64
65 // Progress bar color based on usage
66 ImVec4 bar_color =
67 space.is_critical
69 : (space.is_warning ? GetWarningColor() : GetSuccessColor());
70
71 ImGui::Text("%s:", bank_names[i]);
72 ImGui::SameLine(100);
73
74 // Progress bar
75 {
76 gui::StyleColorGuard bar_guard(ImGuiCol_PlotHistogram, bar_color);
77 float fraction = space.usage_percent / 100.0f;
78 std::string overlay =
79 absl::StrFormat("%d / %d bytes (%.1f%%)", space.used_bytes,
80 space.total_bytes, space.usage_percent);
81 ImGui::ProgressBar(fraction, ImVec2(-1, 0), overlay.c_str());
82 }
83
84 // Warning/critical messages
85 if (space.is_critical) {
86 ImGui::TextColored(GetErrorColor(), ICON_MD_ERROR " %s",
87 space.recommendation.c_str());
88 } else if (space.is_warning) {
89 ImGui::TextColored(GetWarningColor(), ICON_MD_WARNING " %s",
90 space.recommendation.c_str());
91 }
92 }
93
94 // Overall status
95 ImGui::Spacing();
96 if (!bank.AllSongsFit()) {
97 ImGui::TextColored(GetErrorColor(),
98 ICON_MD_ERROR " Some banks are overflowing!");
99 ImGui::TextDisabled(
100 tr("Songs won't fit in ROM. Remove or shorten songs."));
101 } else {
102 ImGui::TextColored(GetSuccessColor(),
103 ICON_MD_CHECK " All songs fit in ROM");
104 }
105
106 ImGui::Unindent(8.0f);
107 }
108
109 ImGui::Separator();
110
111 // Toolbar
112 if (ImGui::Button(ICON_MD_ADD " New Song")) {
113 int new_idx = bank.CreateNewSong("New Song", MusicBank::Bank::Dungeon);
114 if (new_idx >= 0) {
115 selected_song_index_ = new_idx;
117 on_song_selected_(new_idx);
118 if (on_edit_)
119 on_edit_();
120 }
121 }
122 ImGui::SameLine();
123 if (ImGui::Button(ICON_MD_FILE_UPLOAD " Import")) {
124 // TODO: Implement SPC/MML import
125 }
126
127 ImGui::Separator();
128
129 ImGui::BeginChild("SongList", ImVec2(0, 0), true);
130
131 // Vanilla Songs Section
132 if (ImGui::CollapsingHeader(ICON_MD_LIBRARY_MUSIC " Vanilla Songs",
133 ImGuiTreeNodeFlags_DefaultOpen)) {
134 const float item_height = ImGui::GetTextLineHeightWithSpacing();
135 ImGuiListClipper clipper;
136 clipper.Begin(static_cast<int>(filtered_vanilla_indices_.size()),
137 item_height);
138
139 while (clipper.Step()) {
140 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
141 int i = filtered_vanilla_indices_[row];
142 const auto* song = bank.GetSong(i);
143 if (!song)
144 continue;
145
146 std::string display_name =
147 absl::StrFormat("%02X: %s", i + 1, song->name);
148
149 // Icon + label
150 std::string label = absl::StrFormat(ICON_MD_MUSIC_NOTE " %s##vanilla%d",
151 display_name, i);
152 bool is_selected = (selected_song_index_ == i);
153
154 // Push ID to avoid conflicts with same-named items across filter resets
155 ImGui::PushID(i);
156 if (ImGui::Selectable(label.c_str(), is_selected)) {
158 if (on_song_selected_) {
160 }
161 }
162
163 // Double-click opens tracker
164 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
165 if (on_open_tracker_) {
167 }
168 }
169
170 // Context menu for vanilla songs
171 gui::ItemContextMenu(nullptr, [&, i]() {
172 std::vector<gui::MenuItemSpec> items;
173 items.emplace_back("Open Tracker", ICON_MD_MUSIC_NOTE, [this, i]() {
176 });
177 items.emplace_back("Open Piano Roll", ICON_MD_PIANO, [this, i]() {
180 });
181 items.back().separator_after = true;
182 items.emplace_back("Duplicate as Custom", ICON_MD_FILE_COPY,
183 [this, &bank, i]() {
184 bank.DuplicateSong(i);
185 if (on_edit_)
186 on_edit_();
187 // Force cache rebuild on next frame.
188 last_search_buffer_.clear();
189 });
190 items.back().separator_after = true;
191 items.emplace_back("Export to ASM...", ICON_MD_FILE_DOWNLOAD,
192 [this, i]() {
193 if (on_export_asm_)
195 });
196 return items;
197 });
198 ImGui::PopID();
199 }
200 }
201 }
202
203 // Custom Songs Section
204 if (ImGui::CollapsingHeader(ICON_MD_EDIT " Custom Songs",
205 ImGuiTreeNodeFlags_DefaultOpen)) {
206 if (filtered_custom_indices_.empty()) {
207 ImGui::TextDisabled(tr("No custom songs match filter"));
208 ImGui::TextDisabled(tr("Click 'New Song' or duplicate a vanilla song"));
209 } else {
210 const float item_height = ImGui::GetTextLineHeightWithSpacing();
211 ImGuiListClipper clipper;
212 clipper.Begin(static_cast<int>(filtered_custom_indices_.size()),
213 item_height);
214
215 while (clipper.Step()) {
216 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
217 int i = filtered_custom_indices_[row];
218 const auto* song = bank.GetSong(i);
219 if (!song)
220 continue;
221
222 std::string display_name =
223 absl::StrFormat("%02X: %s", i + 1, song->name);
224
225 // Custom song icon + label (different color)
226 std::string label = absl::StrFormat(
227 ICON_MD_AUDIOTRACK " %s##custom%d", display_name, i);
228 bool is_selected = (selected_song_index_ == i);
229
230 gui::StyleColorGuard text_guard(ImGuiCol_Text, GetSuccessColor());
231 ImGui::PushID(i);
232 if (ImGui::Selectable(label.c_str(), is_selected)) {
234 if (on_song_selected_) {
236 }
237 }
238
239 // Double-click opens tracker
240 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
241 if (on_open_tracker_) {
243 }
244 }
245
246 // Context menu for custom songs (includes delete/rename)
247 gui::ItemContextMenu(nullptr, [&, i]() {
248 std::vector<gui::MenuItemSpec> items;
249 items.emplace_back("Open Tracker", ICON_MD_MUSIC_NOTE, [this, i]() {
252 });
253 items.emplace_back("Open Piano Roll", ICON_MD_PIANO, [this, i]() {
256 });
257 items.back().separator_after = true;
258 items.emplace_back("Duplicate", ICON_MD_FILE_COPY,
259 [this, &bank, i]() {
260 bank.DuplicateSong(i);
261 if (on_edit_)
262 on_edit_();
263 last_search_buffer_.clear(); // Force rebuild.
264 });
265 items.emplace_back(
267 [this, &bank, i]() {
269 rename_popup_open_ = true;
270 const auto* rename_song = bank.GetSong(i);
271 if (rename_song) {
272 std::strncpy(rename_buffer_, rename_song->name.c_str(),
273 sizeof(rename_buffer_) - 1);
274 rename_buffer_[sizeof(rename_buffer_) - 1] = '\0';
275 }
276 });
277 items.back().separator_after = true;
278 items.emplace_back("Export to ASM...", ICON_MD_FILE_DOWNLOAD,
279 [this, i]() {
280 if (on_export_asm_)
282 });
283 items.emplace_back("Import from ASM...", ICON_MD_FILE_UPLOAD,
284 [this, i]() {
285 if (on_import_asm_)
287 });
288 items.back().separator_after = true;
289 // Deleting a custom song has no undo; require confirmation.
290 items.push_back(gui::MenuItemSpec::Destructive(
291 "Delete Song...", ICON_MD_DELETE, [this, &bank, i]() {
292 (void)bank.DeleteSong(i);
293 if (selected_song_index_ == i) {
295 }
296 if (on_edit_)
297 on_edit_();
298 last_search_buffer_.clear(); // Force rebuild.
299 }));
300 return items;
301 });
302 ImGui::PopID();
303 }
304 }
305 }
306 }
307
308 ImGui::EndChild();
309
310 // Rename popup (rendered at top level so it isn't clipped by the child)
311 if (rename_popup_open_) {
312 ImGui::OpenPopup("Rename Song");
313 rename_popup_open_ = false;
314 }
315
316 if (ImGui::BeginPopupModal("Rename Song", nullptr,
317 ImGuiWindowFlags_AlwaysAutoResize)) {
318 ImGui::Text(tr("Enter a new name for the song:"));
319 ImGui::SetNextItemWidth(300);
320
321 bool enter_pressed = ImGui::InputText("##RenameSongInput", rename_buffer_,
322 sizeof(rename_buffer_),
323 ImGuiInputTextFlags_EnterReturnsTrue);
324
325 // Focus the input on first appearance
326 if (ImGui::IsWindowAppearing()) {
327 ImGui::SetKeyboardFocusHere(-1);
328 }
329
330 bool apply = enter_pressed || ImGui::Button(tr("OK"));
331 ImGui::SameLine();
332 bool cancel = ImGui::Button(tr("Cancel"));
333
334 if (apply && rename_target_index_ >= 0) {
335 auto* target_song = bank.GetSong(rename_target_index_);
336 if (target_song && std::strlen(rename_buffer_) > 0) {
337 target_song->name = rename_buffer_;
338 target_song->modified = true;
339 if (on_edit_)
340 on_edit_();
341 last_search_buffer_.clear(); // Force filter rebuild.
342 }
344 ImGui::CloseCurrentPopup();
345 }
346
347 if (cancel) {
349 ImGui::CloseCurrentPopup();
350 }
351
352 ImGui::EndPopup();
353 }
354}
355
356bool SongBrowserView::MatchesSearch(const std::string& name) const {
357 if (search_buffer_[0] == '\0')
358 return true;
359
360 // Case-insensitive search
361 std::string lower_name = name;
362 std::string lower_search(search_buffer_);
363 std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(),
364 ::tolower);
365 std::transform(lower_search.begin(), lower_search.end(), lower_search.begin(),
366 ::tolower);
367
368 return lower_name.find(lower_search) != std::string::npos;
369}
370
374
375 for (size_t i = 0; i < bank.GetSongCount(); ++i) {
376 const auto* song = bank.GetSong(static_cast<int>(i));
377 if (!song)
378 continue;
379
380 std::string display_name = absl::StrFormat("%02X: %s", i + 1, song->name);
381 if (!MatchesSearch(display_name))
382 continue;
383
384 if (bank.IsVanilla(static_cast<int>(i))) {
385 filtered_vanilla_indices_.push_back(static_cast<int>(i));
386 } else {
387 filtered_custom_indices_.push_back(static_cast<int>(i));
388 }
389 }
390}
391
392} // namespace music
393} // namespace editor
394} // namespace yaze
std::function< void(int)> on_open_piano_roll_
std::function< void(int)> on_song_selected_
std::function< void(int)> on_export_asm_
bool MatchesSearch(const std::string &name) const
std::function< void(int)> on_open_tracker_
void RebuildFilterCache(const MusicBank &bank)
void Draw(MusicBank &bank)
Draw the song browser.
std::function< void(int)> on_import_asm_
RAII guard for ImGui style colors.
Definition style_guard.h:27
Manages the collection of songs, instruments, and samples from a ROM.
Definition music_bank.h:27
bool IsVanilla(int index) const
Check if a song is a vanilla (original) song.
MusicSong * GetSong(int index)
Get a song by index.
size_t GetSongCount() const
Get the number of songs loaded.
Definition music_bank.h:97
const ExpandedBankInfo & GetExpandedBankInfo() const
Get information about the expanded bank configuration.
Definition music_bank.h:160
bool AllSongsFit() const
Check if all songs fit in their banks.
absl::Status DeleteSong(int index)
Delete a song by index.
SpaceInfo CalculateSpaceUsage(Bank bank) const
Calculate space usage for a bank.
int CreateNewSong(const std::string &name, Bank bank)
Create a new empty song.
bool HasExpandedMusicPatch() const
Check if the ROM has the Oracle of Secrets expanded music patch.
Definition music_bank.h:147
int DuplicateSong(int index)
Duplicate a song.
#define ICON_MD_PIANO
Definition icons.h:1462
#define ICON_MD_LIBRARY_MUSIC
Definition icons.h:1080
#define ICON_MD_STORAGE
Definition icons.h:1865
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_FILE_DOWNLOAD
Definition icons.h:744
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_AUDIOTRACK
Definition icons.h:213
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_FILE_COPY
Definition icons.h:743
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_DRIVE_FILE_RENAME_OUTLINE
Definition icons.h:630
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
bool ItemContextMenu(const char *id, const std::function< std::vector< MenuItemSpec >()> &build, ImGuiPopupFlags flags)
Right-click menu for the last submitted ImGui item.
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
static CanvasMenuItem Destructive(const std::string &lbl, const std::string &ico, std::function< void()> cb, bool confirm=true, const std::string &sc="")