138 lines
7.1 KiB
C++
138 lines
7.1 KiB
C++
|
|
#include "piano_roll.hpp"
|
||
|
|
#include <FL/Fl.H>
|
||
|
|
#include <FL/fl_draw.H>
|
||
|
|
#include <FL/fl_ask.H>
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cstdio>
|
||
|
|
namespace { constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; }
|
||
|
|
PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64_t clip, BeforeEdit before, void *context)
|
||
|
|
: Fl_Widget(left, top, 900, header + 128 * row_height), project_(project), clip_(clip), before_(before), context_(context)
|
||
|
|
{ set_project(project, clip); }
|
||
|
|
void PianoRollWidget::set_project(xtc_project *project, uint64_t clip)
|
||
|
|
{
|
||
|
|
if (project != project_ || clip != clip_) mode_ = 0;
|
||
|
|
project_ = project; clip_ = clip;
|
||
|
|
xtc_clip_info c{};
|
||
|
|
int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK ?
|
||
|
|
static_cast<int>(c.source_length_tick * scale / 960) + key_width + 20 : 900;
|
||
|
|
size(std::max(900, width), header + 128 * row_height);
|
||
|
|
redraw();
|
||
|
|
}
|
||
|
|
uint64_t PianoRollWidget::tick(int px) const
|
||
|
|
{ return static_cast<uint64_t>(std::max(0, px - x() - key_width)) * 960U / scale / 240U * 240U; }
|
||
|
|
xtc_result PianoRollWidget::change(xtc_result result)
|
||
|
|
{
|
||
|
|
if (result == XTC_OK) { do_callback(); redraw(); }
|
||
|
|
else fl_alert("Note unchanged: %s", xtc_result_string(result));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch,
|
||
|
|
unsigned int velocity, unsigned int channel)
|
||
|
|
{
|
||
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||
|
|
uint64_t id;
|
||
|
|
if (r == XTC_OK) r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id);
|
||
|
|
if (r == XTC_OK) selected_ = id;
|
||
|
|
return change(r);
|
||
|
|
}
|
||
|
|
xtc_result PianoRollWidget::delete_selected()
|
||
|
|
{
|
||
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||
|
|
if (r == XTC_OK) r = xtc_project_remove_note(project_, selected_);
|
||
|
|
if (r == XTC_OK) selected_ = UINT64_MAX;
|
||
|
|
return change(r);
|
||
|
|
}
|
||
|
|
void PianoRollWidget::draw()
|
||
|
|
{
|
||
|
|
fl_push_clip(x(), y(), w(), h());
|
||
|
|
fl_color(fl_rgb_color(30, 33, 38)); fl_rectf(x(), y(), w(), h());
|
||
|
|
xtc_clip_info clip{};
|
||
|
|
if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) { fl_pop_clip(); return; }
|
||
|
|
int cx, cy, cw, ch; fl_clip_box(x(), y(), w(), h(), cx, cy, cw, ch);
|
||
|
|
fl_font(FL_HELVETICA, 11);
|
||
|
|
for (int pitch = 0; pitch < 128; ++pitch) {
|
||
|
|
int py = y() + header + (127 - pitch) * row_height;
|
||
|
|
if (py + row_height < cy || py > cy + ch) continue;
|
||
|
|
int pc = pitch % 12;
|
||
|
|
bool black = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10;
|
||
|
|
fl_color(black ? fl_rgb_color(29, 31, 35) : fl_rgb_color(43, 46, 52));
|
||
|
|
fl_rectf(cx, py, cw, row_height - 1);
|
||
|
|
fl_color(black ? fl_rgb_color(60, 63, 70) : fl_rgb_color(130, 135, 140));
|
||
|
|
fl_rectf(x(), py, key_width - 2, row_height - 1);
|
||
|
|
if (!pc) {
|
||
|
|
char text[20]; std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1);
|
||
|
|
fl_color(FL_WHITE); fl_draw(text, x() + 6, py + 12);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
int first = std::max(0, (cx - x() - key_width) / (scale / 4));
|
||
|
|
int last = (cx + cw - x() - key_width) / (scale / 4) + 1;
|
||
|
|
for (int step = first; step <= last; ++step) {
|
||
|
|
int px = x() + key_width + step * (scale / 4);
|
||
|
|
fl_color(step % 16 == 0 ? fl_rgb_color(95, 98, 105) : fl_rgb_color(52, 55, 62));
|
||
|
|
fl_line(px, cy, px, cy + ch);
|
||
|
|
}
|
||
|
|
size_t count = 0; xtc_project_note_count(project_, &count);
|
||
|
|
for (size_t i = 0; i < count; ++i) {
|
||
|
|
xtc_note_info n{}; xtc_project_get_note(project_, i, &n);
|
||
|
|
if (n.clip_id != clip_) continue;
|
||
|
|
int left = x() + key_width + static_cast<int>(n.start_tick * scale / 960);
|
||
|
|
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
||
|
|
int py = y() + header + (127 - static_cast<int>(n.pitch)) * row_height;
|
||
|
|
int a = std::max(cx, left), b = std::min(cx + cw, right);
|
||
|
|
if (a >= b || py + row_height < cy || py > cy + ch) continue;
|
||
|
|
fl_color(n.id == selected_ ? fl_rgb_color(230, 190, 80) : fl_rgb_color(90, 175, 130));
|
||
|
|
fl_rectf(a, py + 1, b - a, row_height - 3);
|
||
|
|
}
|
||
|
|
fl_pop_clip();
|
||
|
|
}
|
||
|
|
int PianoRollWidget::handle(int event)
|
||
|
|
{
|
||
|
|
if (event == FL_PUSH) {
|
||
|
|
take_focus(); mode_ = 0;
|
||
|
|
size_t count = 0; xtc_project_note_count(project_, &count);
|
||
|
|
selected_ = UINT64_MAX;
|
||
|
|
for (size_t i = count; i > 0; --i) {
|
||
|
|
xtc_note_info n{}; xtc_project_get_note(project_, i - 1, &n);
|
||
|
|
int left = x() + key_width + static_cast<int>(n.start_tick * scale / 960);
|
||
|
|
int right = left + std::max(3, static_cast<int>(n.length_tick * scale / 960));
|
||
|
|
int top = y() + header + (127 - static_cast<int>(n.pitch)) * row_height;
|
||
|
|
if (n.clip_id == clip_ && Fl::event_x() >= left && Fl::event_x() < right &&
|
||
|
|
Fl::event_y() >= top && Fl::event_y() < top + row_height) {
|
||
|
|
selected_ = n.id; original_ = n; drag_x_ = Fl::event_x(); drag_y_ = Fl::event_y();
|
||
|
|
mode_ = right - Fl::event_x() <= 6 ? 2 : 1; break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (selected_ != UINT64_MAX && Fl::event_button() == FL_RIGHT_MOUSE) { delete_selected(); mode_ = 0; }
|
||
|
|
else if (Fl::event_button() == FL_LEFT_MOUSE && selected_ == UINT64_MAX &&
|
||
|
|
Fl::event_x() >= x() + key_width && Fl::event_y() >= y() + header) {
|
||
|
|
int pitch = 127 - (Fl::event_y() - y() - header) / row_height;
|
||
|
|
xtc_clip_info c{}; uint64_t start = tick(Fl::event_x());
|
||
|
|
if (pitch >= 0 && pitch <= 127 && xtc_project_find_clip(project_, clip_, &c) == XTC_OK && start < c.source_length_tick)
|
||
|
|
add_note(start, std::min<uint64_t>(960, c.source_length_tick - start), static_cast<unsigned int>(pitch));
|
||
|
|
}
|
||
|
|
redraw(); return 1;
|
||
|
|
}
|
||
|
|
if (event == FL_DRAG) return mode_ != 0;
|
||
|
|
if (event == FL_RELEASE && mode_) {
|
||
|
|
int mode = mode_; mode_ = 0;
|
||
|
|
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale / 240 * 240;
|
||
|
|
xtc_clip_info c{}; if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK) return 1;
|
||
|
|
int64_t start = static_cast<int64_t>(original_.start_tick), length = static_cast<int64_t>(original_.length_tick);
|
||
|
|
unsigned int pitch = original_.pitch;
|
||
|
|
if (mode == 1) {
|
||
|
|
start = std::clamp<int64_t>(start + delta, 0, static_cast<int64_t>(c.source_length_tick) - length);
|
||
|
|
pitch = static_cast<unsigned int>(std::clamp(static_cast<int>(pitch) - (Fl::event_y() - drag_y_) / row_height, 0, 127));
|
||
|
|
} else {
|
||
|
|
int64_t maximum = static_cast<int64_t>(c.source_length_tick) - start;
|
||
|
|
length = std::clamp<int64_t>(length + delta, std::min<int64_t>(240, maximum), maximum);
|
||
|
|
}
|
||
|
|
xtc_result r = before_ ? before_(context_) : XTC_OK;
|
||
|
|
if (r == XTC_OK) r = xtc_project_edit_note(project_, selected_, static_cast<uint64_t>(start),
|
||
|
|
static_cast<uint64_t>(length), pitch, original_.velocity, original_.channel);
|
||
|
|
change(r); return 1;
|
||
|
|
}
|
||
|
|
if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { delete_selected(); return 1; }
|
||
|
|
if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { mode_ = 0; return 1; }
|
||
|
|
if (event == FL_FOCUS || event == FL_UNFOCUS) return 1;
|
||
|
|
return Fl_Widget::handle(event);
|
||
|
|
}
|