156 uint32_t duration = segment.GetDuration();
159 if (ImGui::BeginTable(
"TrackerTable", 9,
160 ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY |
161 ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) {
164 ImGui::TableSetupColumn(
"Tick", ImGuiTableColumnFlags_WidthFixed, 50.0f);
165 for (
int i = 0; i < 8; ++i) {
166 ImGui::TableSetupColumn(absl::StrFormat(
"Ch %d", i + 1).c_str());
168 ImGui::TableHeadersRow();
172 ImGuiListClipper clipper;
175 while (clipper.Step()) {
176 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
180 ImGui::TableNextRow();
184 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, GetColorBeatHighlight());
188 ImGui::TableSetColumnIndex(0);
189 ImGui::TextDisabled(
"%04X", tick_start);
192 for (
int ch = 0; ch < 8; ++ch) {
193 ImGui::TableSetColumnIndex(ch + 1);
205 if (row >= row_start && row <= row_end && (ch + 1) >= col_start && (ch + 1) <= col_end) {
206 ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
207 GetColorSelection(
true));
209 }
else if (is_selected) {
210 ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
211 GetColorSelection(
false));
214 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
221 int event_index = -1;
222 auto& track = segment.tracks[ch];
223 for (
size_t idx = 0; idx < track.events.size(); ++idx) {
224 const auto& evt = track.events[idx];
225 if (evt.tick >= tick_start && evt.tick < tick_end) {
226 event_index =
static_cast<int>(idx);
229 if (evt.tick >= tick_end)
break;
236 ImGui::PushID(row * 100 + ch);
237 if (ImGui::Selectable(
"##cell",
false,
238 ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap,
240 if (ImGui::GetIO().KeyShift) {
261 TrackEvent* event_ptr = (event_index >= 0 && event_index < static_cast<int>(track.
events.size()))
262 ? &track.
events[event_index]
265 bool has_event = event_ptr !=
nullptr;
268 ImGui::TextDisabled(
"...");
270 auto&
event = *event_ptr;
271 switch (event.type) {
272 case TrackEvent::Type::Note:
273 ImGui::TextColored(ImColor(GetColorNote()),
"%s", event.note.GetNoteName().c_str());
274 if (ImGui::IsItemHovered()) {
275 ImGui::SetTooltip(
"Note: %s\nDuration: %d\nVelocity: %d",
276 event.note.GetNoteName().c_str(),
278 event.note.velocity);
282 case TrackEvent::Type::Command: {
283 ImU32 color = GetColorCommand();
284 std::string label = absl::StrFormat(
"CMD %02X", event.command.opcode);
285 std::string tooltip = DescribeCommand(event.command.opcode);
288 if (event.command.opcode == 0xE0) {
290 const auto* inst = bank->
GetInstrument(event.command.params[0]);
292 label = absl::StrFormat(
"Instr: %s", inst->name.c_str());
293 tooltip = absl::StrFormat(
"Set Instrument: %s (ID %02X)", inst->name.c_str(), event.command.params[0]);
295 label = absl::StrFormat(
"Instr: %02X", event.command.params[0]);
298 label = absl::StrFormat(
"Instr: %02X", event.command.params[0]);
300 }
else if (event.command.opcode == 0xE1) {
301 int pan =
event.command.params[0];
302 if (pan == 0x0A) label =
"Pan: Center";
303 else if (pan < 0x0A) label = absl::StrFormat(
"Pan: L%d", 0x0A - pan);
304 else label = absl::StrFormat(
"Pan: R%d", pan - 0x0A);
305 }
else if (event.command.opcode == 0xE7) {
306 label = absl::StrFormat(
"Tempo: %d", event.command.params[0]);
307 }
else if (event.command.opcode == 0xED) {
308 label = absl::StrFormat(
"Vol: %d", event.command.params[0]);
309 }
else if (event.command.opcode == 0xE5) {
310 label = absl::StrFormat(
"M.Vol: %d", event.command.params[0]);
313 ImGui::TextColored(ImColor(color),
"%s", label.c_str());
314 if (ImGui::IsItemHovered()) {
315 ImGui::SetTooltip(
"%s\nOpcode: %02X\nParams: %02X %02X %02X",
317 event.command.opcode,
318 event.command.params[0],
319 event.command.params[1],
320 event.command.params[2]);
325 case TrackEvent::Type::SubroutineCall:
326 ImGui::TextColored(ImColor(GetColorSubroutine()),
"CALL");
329 case TrackEvent::Type::End:
330 ImGui::TextDisabled(
"END");
335 bool hovered = ImGui::IsItemHovered();
336 const bool double_clicked =
337 hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
338 const bool right_clicked =
339 hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
342 if (!has_event && double_clicked) {
350 const std::string popup_id =
351 absl::StrFormat(
"EditEvent##%d_%d_%d", channel_idx, tick, event_index);
352 if ((double_clicked || right_clicked) && has_event) {
353 ImGui::OpenPopup(popup_id.c_str());
356 if (ImGui::BeginPopup(popup_id.c_str())) {
358 ImGui::TextDisabled(
"Empty");
363 auto&
event = *event_ptr;
364 if (event.type == TrackEvent::Type::Note) {
368 edit_duration =
event.note.duration;
370 static std::vector<std::string> note_labels;
371 if (note_labels.empty()) {
373 Note n; n.
pitch =
static_cast<uint8_t
>(p);
378 idx = std::clamp(idx, 0,
static_cast<int>(note_labels.size()) - 1);
380 ImGui::Text(
"Edit Note");
381 if (ImGui::BeginCombo(
"Pitch", note_labels[idx].c_str())) {
382 for (
int i = 0; i < static_cast<int>(note_labels.size()); ++i) {
383 bool sel = (i == idx);
384 if (ImGui::Selectable(note_labels[i].c_str(), sel)) {
388 if (sel) ImGui::SetItemDefaultFocus();
393 if (ImGui::SliderInt(
"Duration", &edit_duration, 1, 0xFF)) {
394 edit_duration = std::clamp(edit_duration, 1, 0xFF);
397 if (ImGui::Button(
"Apply")) {
398 event.note.pitch =
static_cast<uint8_t
>(edit_pitch);
399 event.note.duration =
static_cast<uint8_t
>(edit_duration);
401 ImGui::CloseCurrentPopup();
403 }
else if (event.type == TrackEvent::Type::Command) {
404 int current_cmd_idx = 0;
405 for (
size_t i = 0; i < IM_ARRAYSIZE(kCommandOptions); ++i) {
406 if (kCommandOptions[i].opcode == event.command.
opcode) {
407 current_cmd_idx =
static_cast<int>(i);
411 ImGui::Text(
"Edit Command");
412 if (ImGui::BeginCombo(
"Opcode", kCommandOptions[current_cmd_idx].name)) {
413 for (
size_t i = 0; i < IM_ARRAYSIZE(kCommandOptions); ++i) {
414 bool sel = (
static_cast<int>(i) == current_cmd_idx);
415 if (ImGui::Selectable(kCommandOptions[i].name, sel)) {
416 current_cmd_idx =
static_cast<int>(i);
418 if (sel) ImGui::SetItemDefaultFocus();
423 int p0 =
event.command.params[0];
424 int p1 =
event.command.params[1];
425 int p2 =
event.command.params[2];
427 uint8_t opcode = kCommandOptions[current_cmd_idx].
opcode;
429 if (opcode == 0xE0 && bank) {
432 std::string preview = inst ? absl::StrFormat(
"%02X: %s", p0, inst->name.c_str()) : absl::StrFormat(
"%02X", p0);
433 if (ImGui::BeginCombo(
"Instrument", preview.c_str())) {
436 bool is_selected = (
static_cast<int>(i) == p0);
437 if (ImGui::Selectable(absl::StrFormat(
"%02X: %s", i, item->name.c_str()).c_str(), is_selected)) {
438 p0 =
static_cast<int>(i);
440 if (is_selected) ImGui::SetItemDefaultFocus();
445 ImGui::InputInt(
"Param 0 (hex)", &p0, 1, 4, ImGuiInputTextFlags_CharsHexadecimal);
448 ImGui::InputInt(
"Param 1 (hex)", &p1, 1, 4, ImGuiInputTextFlags_CharsHexadecimal);
449 ImGui::InputInt(
"Param 2 (hex)", &p2, 1, 4, ImGuiInputTextFlags_CharsHexadecimal);
451 if (ImGui::Button(
"Apply")) {
452 event.command.opcode = opcode;
453 event.command.params[0] =
static_cast<uint8_t
>(p0 & 0xFF);
454 event.command.params[1] =
static_cast<uint8_t
>(p1 & 0xFF);
455 event.command.params[2] =
static_cast<uint8_t
>(p2 & 0xFF);
457 ImGui::CloseCurrentPopup();
460 ImGui::TextDisabled(
"%s", DescribeCommand(event.command.opcode).c_str());
462 ImGui::TextDisabled(
"Unsupported edit type");
527 if (!song || song->
segments.empty())
return;
529 if (
selected_row_ < 0 || selected_col_ < 1 || selected_col_ > 8)
return;
532 auto& track = song->
segments[0].tracks[ch];
536 auto TriggerEdit = [
this]() {
543 struct KeyNote { ImGuiKey key;
int semitone;
int octave_offset; };
544 static const KeyNote key_map[] = {
545 {ImGuiKey_Z, 0, 0}, {ImGuiKey_S, 1, 0}, {ImGuiKey_X, 2, 0}, {ImGuiKey_D, 3, 0},
546 {ImGuiKey_C, 4, 0}, {ImGuiKey_V, 5, 0}, {ImGuiKey_G, 6, 0}, {ImGuiKey_B, 7, 0},
547 {ImGuiKey_H, 8, 0}, {ImGuiKey_N, 9, 0}, {ImGuiKey_J, 10, 0}, {ImGuiKey_M, 11, 0},
548 {ImGuiKey_Q, 0, 1}, {ImGuiKey_2, 1, 1}, {ImGuiKey_W, 2, 1}, {ImGuiKey_3, 3, 1},
549 {ImGuiKey_E, 4, 1}, {ImGuiKey_R, 5, 1}, {ImGuiKey_5, 6, 1}, {ImGuiKey_T, 7, 1},
550 {ImGuiKey_6, 8, 1}, {ImGuiKey_Y, 9, 1}, {ImGuiKey_7, 10, 1}, {ImGuiKey_U, 11, 1}
553 static int base_octave = 4;
556 if (ImGui::IsKeyPressed(ImGuiKey_F1)) base_octave = std::max(1, base_octave - 1);
557 if (ImGui::IsKeyPressed(ImGuiKey_F2)) base_octave = std::min(6, base_octave + 1);
560 for (
const auto& kn : key_map) {
561 if (ImGui::IsKeyPressed(kn.key)) {
564 int octave = base_octave + kn.octave_offset;
565 if (octave > 6) octave = 6;
567 uint8_t pitch =
kNoteMinPitch + (octave - 1) * 12 + kn.semitone;
571 for (
auto& evt : track.events) {
572 if (evt.tick == tick) {
573 if (evt.type == TrackEvent::Type::Note) {
574 evt.note.pitch = pitch;
595 if (ImGui::IsKeyPressed(ImGuiKey_Delete) || ImGui::IsKeyPressed(ImGuiKey_Backspace)) {
596 bool changed =
false;
603 for (
size_t i = 0; i < track.events.size(); ++i) {
604 if (track.events[i].tick == tick) {
606 track.RemoveEvent(i);
619 if (ImGui::IsKeyPressed(ImGuiKey_Space)) {