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"
11#include "imgui/imgui.h"
12
13namespace yaze {
14namespace editor {
15namespace music {
16
21
23 // Search filter
24 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
25 ImGui::InputTextWithHint("##SongFilter", ICON_MD_SEARCH " Search songs...",
27
28 // Update filter cache if needed
29 if (std::string(search_buffer_) != last_search_buffer_ ||
33 }
34
35 // Bank Space Management Section
36 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " Bank Space")) {
37 ImGui::Indent(8.0f);
38
39 // Check for expanded music patch
40 if (bank.HasExpandedMusicPatch()) {
41 ImGui::TextColored(GetSuccessColor(), ICON_MD_CHECK_CIRCLE
42 " Oracle of Secrets expanded music detected");
43 const auto& info = bank.GetExpandedBankInfo();
44 ImGui::TextDisabled(tr("Expanded bank at $%06X, Aux at $%06X"),
45 info.main_rom_offset, info.aux_rom_offset);
46 ImGui::Spacing();
47 }
48
49 // Display space for each bank
50 static const char* bank_names[] = {"Overworld", "Dungeon", "Credits",
51 "Expanded", "Auxiliary"};
52 static const MusicBank::Bank banks[] = {
56
57 int num_banks = bank.HasExpandedMusicPatch() ? 5 : 3;
58
59 for (int i = 0; i < num_banks; ++i) {
60 auto space = bank.CalculateSpaceUsage(banks[i]);
61 if (space.total_bytes == 0)
62 continue; // Skip empty/invalid banks
63
64 // Progress bar color based on usage
65 ImVec4 bar_color =
66 space.is_critical
68 : (space.is_warning ? GetWarningColor() : GetSuccessColor());
69
70 ImGui::Text("%s:", bank_names[i]);
71 ImGui::SameLine(100);
72
73 // Progress bar
74 {
75 gui::StyleColorGuard bar_guard(ImGuiCol_PlotHistogram, bar_color);
76 float fraction = space.usage_percent / 100.0f;
77 std::string overlay =
78 absl::StrFormat("%d / %d bytes (%.1f%%)", space.used_bytes,
79 space.total_bytes, space.usage_percent);
80 ImGui::ProgressBar(fraction, ImVec2(-1, 0), overlay.c_str());
81 }
82
83 // Warning/critical messages
84 if (space.is_critical) {
85 ImGui::TextColored(GetErrorColor(), ICON_MD_ERROR " %s",
86 space.recommendation.c_str());
87 } else if (space.is_warning) {
88 ImGui::TextColored(GetWarningColor(), ICON_MD_WARNING " %s",
89 space.recommendation.c_str());
90 }
91 }
92
93 // Overall status
94 ImGui::Spacing();
95 if (!bank.AllSongsFit()) {
96 ImGui::TextColored(GetErrorColor(),
97 ICON_MD_ERROR " Some banks are overflowing!");
98 ImGui::TextDisabled(
99 tr("Songs won't fit in ROM. Remove or shorten songs."));
100 } else {
101 ImGui::TextColored(GetSuccessColor(),
102 ICON_MD_CHECK " All songs fit in ROM");
103 }
104
105 ImGui::Unindent(8.0f);
106 }
107
108 ImGui::Separator();
109
110 // Toolbar
111 if (ImGui::Button(ICON_MD_ADD " New Song")) {
112 int new_idx = bank.CreateNewSong("New Song", MusicBank::Bank::Dungeon);
113 if (new_idx >= 0) {
114 selected_song_index_ = new_idx;
116 on_song_selected_(new_idx);
117 if (on_edit_)
118 on_edit_();
119 }
120 }
121 ImGui::SameLine();
122 if (ImGui::Button(ICON_MD_FILE_UPLOAD " Import")) {
123 // TODO: Implement SPC/MML import
124 }
125
126 ImGui::Separator();
127
128 ImGui::BeginChild("SongList", ImVec2(0, 0), true);
129
130 // Vanilla Songs Section
131 if (ImGui::CollapsingHeader(ICON_MD_LIBRARY_MUSIC " Vanilla Songs",
132 ImGuiTreeNodeFlags_DefaultOpen)) {
133 const float item_height = ImGui::GetTextLineHeightWithSpacing();
134 ImGuiListClipper clipper;
135 clipper.Begin(static_cast<int>(filtered_vanilla_indices_.size()),
136 item_height);
137
138 while (clipper.Step()) {
139 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
140 int i = filtered_vanilla_indices_[row];
141 const auto* song = bank.GetSong(i);
142 if (!song)
143 continue;
144
145 std::string display_name =
146 absl::StrFormat("%02X: %s", i + 1, song->name);
147
148 // Icon + label
149 std::string label = absl::StrFormat(ICON_MD_MUSIC_NOTE " %s##vanilla%d",
150 display_name, i);
151 bool is_selected = (selected_song_index_ == i);
152
153 // Push ID to avoid conflicts with same-named items across filter resets
154 ImGui::PushID(i);
155 if (ImGui::Selectable(label.c_str(), is_selected)) {
157 if (on_song_selected_) {
159 }
160 }
161
162 // Double-click opens tracker
163 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
164 if (on_open_tracker_) {
166 }
167 }
168
169 // Context menu for vanilla songs
170 if (ImGui::BeginPopupContextItem()) {
171 if (ImGui::MenuItem(ICON_MD_MUSIC_NOTE " Open Tracker")) {
174 }
175 if (ImGui::MenuItem(ICON_MD_PIANO " Open Piano Roll")) {
178 }
179 ImGui::Separator();
180 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Duplicate as Custom")) {
181 bank.DuplicateSong(i);
182 if (on_edit_)
183 on_edit_();
184 // Force cache rebuild on next frame (or immediately).
185 last_search_buffer_.clear();
186 }
187 ImGui::Separator();
188 if (ImGui::MenuItem(ICON_MD_FILE_DOWNLOAD " Export to ASM...")) {
189 if (on_export_asm_)
191 }
192 ImGui::EndPopup();
193 }
194 ImGui::PopID();
195 }
196 }
197 }
198
199 // Custom Songs Section
200 if (ImGui::CollapsingHeader(ICON_MD_EDIT " Custom Songs",
201 ImGuiTreeNodeFlags_DefaultOpen)) {
202 if (filtered_custom_indices_.empty()) {
203 ImGui::TextDisabled(tr("No custom songs match filter"));
204 ImGui::TextDisabled(tr("Click 'New Song' or duplicate a vanilla song"));
205 } else {
206 const float item_height = ImGui::GetTextLineHeightWithSpacing();
207 ImGuiListClipper clipper;
208 clipper.Begin(static_cast<int>(filtered_custom_indices_.size()),
209 item_height);
210
211 while (clipper.Step()) {
212 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
213 int i = filtered_custom_indices_[row];
214 const auto* song = bank.GetSong(i);
215 if (!song)
216 continue;
217
218 std::string display_name =
219 absl::StrFormat("%02X: %s", i + 1, song->name);
220
221 // Custom song icon + label (different color)
222 std::string label = absl::StrFormat(
223 ICON_MD_AUDIOTRACK " %s##custom%d", display_name, i);
224 bool is_selected = (selected_song_index_ == i);
225
226 gui::StyleColorGuard text_guard(ImGuiCol_Text, GetSuccessColor());
227 ImGui::PushID(i);
228 if (ImGui::Selectable(label.c_str(), is_selected)) {
230 if (on_song_selected_) {
232 }
233 }
234
235 // Double-click opens tracker
236 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
237 if (on_open_tracker_) {
239 }
240 }
241
242 // Context menu for custom songs (includes delete/rename)
243 if (ImGui::BeginPopupContextItem()) {
244 if (ImGui::MenuItem(ICON_MD_MUSIC_NOTE " Open Tracker")) {
247 }
248 if (ImGui::MenuItem(ICON_MD_PIANO " Open Piano Roll")) {
251 }
252 ImGui::Separator();
253 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Duplicate")) {
254 bank.DuplicateSong(i);
255 if (on_edit_)
256 on_edit_();
257 last_search_buffer_.clear(); // Force rebuild.
258 }
259 if (ImGui::MenuItem(ICON_MD_DRIVE_FILE_RENAME_OUTLINE " Rename")) {
261 rename_popup_open_ = true;
262 const auto* rename_song = bank.GetSong(i);
263 if (rename_song) {
264 std::strncpy(rename_buffer_, rename_song->name.c_str(),
265 sizeof(rename_buffer_) - 1);
266 rename_buffer_[sizeof(rename_buffer_) - 1] = '\0';
267 }
268 }
269 ImGui::Separator();
270 if (ImGui::MenuItem(ICON_MD_FILE_DOWNLOAD " Export to ASM...")) {
271 if (on_export_asm_)
273 }
274 if (ImGui::MenuItem(ICON_MD_FILE_UPLOAD " Import from ASM...")) {
275 if (on_import_asm_)
277 }
278 ImGui::Separator();
279 if (ImGui::MenuItem(ICON_MD_DELETE " Delete")) {
280 (void)bank.DeleteSong(i);
281 if (selected_song_index_ == i) {
283 }
284 if (on_edit_)
285 on_edit_();
286 last_search_buffer_.clear(); // Force rebuild.
287 }
288 ImGui::EndPopup();
289 }
290 ImGui::PopID();
291 }
292 }
293 }
294 }
295
296 ImGui::EndChild();
297
298 // Rename popup (rendered at top level so it isn't clipped by the child)
299 if (rename_popup_open_) {
300 ImGui::OpenPopup("Rename Song");
301 rename_popup_open_ = false;
302 }
303
304 if (ImGui::BeginPopupModal("Rename Song", nullptr,
305 ImGuiWindowFlags_AlwaysAutoResize)) {
306 ImGui::Text(tr("Enter a new name for the song:"));
307 ImGui::SetNextItemWidth(300);
308
309 bool enter_pressed = ImGui::InputText("##RenameSongInput", rename_buffer_,
310 sizeof(rename_buffer_),
311 ImGuiInputTextFlags_EnterReturnsTrue);
312
313 // Focus the input on first appearance
314 if (ImGui::IsWindowAppearing()) {
315 ImGui::SetKeyboardFocusHere(-1);
316 }
317
318 bool apply = enter_pressed || ImGui::Button(tr("OK"));
319 ImGui::SameLine();
320 bool cancel = ImGui::Button(tr("Cancel"));
321
322 if (apply && rename_target_index_ >= 0) {
323 auto* target_song = bank.GetSong(rename_target_index_);
324 if (target_song && std::strlen(rename_buffer_) > 0) {
325 target_song->name = rename_buffer_;
326 target_song->modified = true;
327 if (on_edit_)
328 on_edit_();
329 last_search_buffer_.clear(); // Force filter rebuild.
330 }
332 ImGui::CloseCurrentPopup();
333 }
334
335 if (cancel) {
337 ImGui::CloseCurrentPopup();
338 }
339
340 ImGui::EndPopup();
341 }
342}
343
344bool SongBrowserView::MatchesSearch(const std::string& name) const {
345 if (search_buffer_[0] == '\0')
346 return true;
347
348 // Case-insensitive search
349 std::string lower_name = name;
350 std::string lower_search(search_buffer_);
351 std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(),
352 ::tolower);
353 std::transform(lower_search.begin(), lower_search.end(), lower_search.begin(),
354 ::tolower);
355
356 return lower_name.find(lower_search) != std::string::npos;
357}
358
362
363 for (size_t i = 0; i < bank.GetSongCount(); ++i) {
364 const auto* song = bank.GetSong(static_cast<int>(i));
365 if (!song)
366 continue;
367
368 std::string display_name = absl::StrFormat("%02X: %s", i + 1, song->name);
369 if (!MatchesSearch(display_name))
370 continue;
371
372 if (bank.IsVanilla(static_cast<int>(i))) {
373 filtered_vanilla_indices_.push_back(static_cast<int>(i));
374 } else {
375 filtered_custom_indices_.push_back(static_cast<int>(i));
376 }
377 }
378}
379
380} // namespace music
381} // namespace editor
382} // 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_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_DRIVE_FILE_RENAME_OUTLINE
Definition icons.h:630
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54