xybrid/xybrid/ui/patterneditoritemdelegate.cpp

335 lines
16 KiB
C++

#include "patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;
#include "config/colorscheme.h"
using Xybrid::Config::ColorScheme;
#include "ui/patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include "ui/patterneditormodel.h"
using Xybrid::UI::PatternEditorModel;
#include "editing/compositecommand.h"
#include "editing/patterncommands.h"
using namespace Xybrid::Editing;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include <limits>
#include <QDebug>
#include <QPainter>
#include <QKeyEvent>
namespace {
constexpr int pad = PatternEditorModel::cellPadding;
constexpr int pianoKeys[] = {
Qt::Key_Q, Qt::Key_W, Qt::Key_E, Qt::Key_R, Qt::Key_T, Qt::Key_Y, Qt::Key_U, Qt::Key_I, Qt::Key_O, Qt::Key_P, Qt::Key_BracketLeft, Qt::Key_BracketRight,
Qt::Key_A, Qt::Key_S, Qt::Key_D, Qt::Key_F, Qt::Key_G, Qt::Key_H, Qt::Key_J, Qt::Key_K, Qt::Key_L, Qt::Key_Semicolon, Qt::Key_Apostrophe, Qt::Key_Backslash,
Qt::Key_Z, Qt::Key_X, Qt::Key_C, Qt::Key_V, Qt::Key_B, Qt::Key_N, Qt::Key_M, Qt::Key_Comma, Qt::Key_Period, Qt::Key_Slash,
};
constexpr int numberKeys[] = {
Qt::Key_0, Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5, Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9,
Qt::Key_A, Qt::Key_B, Qt::Key_C, Qt::Key_D, Qt::Key_E, Qt::Key_F,
};
const std::unordered_map<int, int> keyConv = []() {
std::unordered_map<int, int> m;
m[Qt::Key_BraceLeft] = Qt::Key_BracketLeft;
m[Qt::Key_BraceRight] = Qt::Key_BracketRight;
m[Qt::Key_Bar] = Qt::Key_Backslash;
m[Qt::Key_Colon] = Qt::Key_Semicolon;
m[Qt::Key_QuoteDbl] = Qt::Key_Apostrophe;
m[Qt::Key_Less] = Qt::Key_Comma;
m[Qt::Key_Greater] = Qt::Key_Period;
m[Qt::Key_Question] = Qt::Key_Slash;
return m;
}();
template <typename T>
[[maybe_unused]] void insertDigit(T& val, size_t hex) { // insert hex digit into a particular value
if (static_cast<int>(val) < 0) val = 0;
val = static_cast<T>((static_cast<size_t>(val) & 15) * 16 + (hex & 15));
}
struct SelectionBounds {
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
int ch1, ch2;
SelectionBounds(const QModelIndexList& sel) {
x1 = std::numeric_limits<int>::max();
y1 = std::numeric_limits<int>::max();
for (auto s : sel) {
x1 = std::min(x1, s.column());
y1 = std::min(y1, s.row());
x2 = std::max(x2, s.column());
y2 = std::max(y2, s.row());
}
ch1 = (x1 - (x1 % PatternEditorModel::colsPerChannel)) / PatternEditorModel::colsPerChannel;
ch2 = (x2 - (x2 % PatternEditorModel::colsPerChannel)) / PatternEditorModel::colsPerChannel;
}
[[maybe_unused]] bool portSelected(int c) {
int cx = c * PatternEditorModel::colsPerChannel;
return (cx >= x1 && cx <= x2);
}
[[maybe_unused]] bool noteSelected(int c) {
int cx = (c * PatternEditorModel::colsPerChannel) + 1;
return (cx >= x1 && cx <= x2);
}
[[maybe_unused]] bool paramSelected(int c, int p) {
int cx = (c * PatternEditorModel::colsPerChannel) + 2 + (p*2);
return (cx+1 >= x1 && cx <= x2);
}
[[maybe_unused]] int maxParamSelected(int c) {
return std::min((x2 - (c * PatternEditorModel::colsPerChannel) - 2) / 2, PatternEditorModel::paramSoftCap);
}
};
}
void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
{ /* background */ } {
auto p = const_cast<PatternEditorModel*>(static_cast<const PatternEditorModel*>(index.model()))->getPattern();
painter->fillRect(option.rect, ColorScheme::current.patternBg);
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgMeasure);
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgBeat);
}
// selection/cursor highlight
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, ColorScheme::current.patternSelection);
if (option.state & QStyle::State_HasFocus) {
painter->setPen(ColorScheme::current.patternSelection);
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(1,1,-2,-2));
//painter->fillRect(option.rect, ColorScheme::current.patternSelection);
}
// and main data
QString s = index.data().toString();
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
int cc = index.column() % PatternEditorModel::colsPerChannel;
int align = Qt::AlignCenter;
if (cc > 1) { // param field
if (cc % 2 == 0) align = Qt::AlignVCenter | Qt::AlignRight;
else align = Qt::AlignVCenter | Qt::AlignLeft;
}
if (s == QString("» ")) {
align = Qt::AlignVCenter | Qt::AlignLeft;
painter->setPen(ColorScheme::current.patternFgBlank);
} else {
if (s == QString(" - ") || s == QString("- ")) painter->setPen(ColorScheme::current.patternFgBlank);
else if (cc == 0) painter->setPen(ColorScheme::current.patternFgPort);
else if (cc == 1) painter->setPen(ColorScheme::current.patternFgNote);
else if (cc % 2 == 0) painter->setPen(ColorScheme::current.patternFgParamCmd);
else painter->setPen(ColorScheme::current.patternFgParamAmt);
}
painter->drawText(option.rect, align, s);
}
bool PatternEditorItemDelegate::eventFilter(QObject *obj, QEvent *event) {
/*if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
auto* e = static_cast<QKeyEvent*>(event);
if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) {
e->accept();
return true;
}
}//*/
return QObject::eventFilter(obj, event);
}
bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) {
if (index.data().isNull()) return false; // no channels?
auto type = event->type();
if (type == QEvent::KeyRelease) qDebug() << "key release";
if (type == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
auto k = static_cast<QKeyEvent*>(event)->key(); // grab key
if (auto i = keyConv.find(k); i != keyConv.end()) k = i->second;
auto mod = static_cast<QKeyEvent*>(event)->modifiers();
auto m = static_cast<PatternEditorModel*>(model); // we know this will always be pattern editor
auto p = m->getPattern();
int cc = index.column() % PatternEditorModel::colsPerChannel;
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
auto* dc = new PatternDeltaCommand(p, ch, index.row());
auto& row = dc->row;//p->rowAt(ch, index.row());
auto* sm = static_cast<PatternEditorView*>(parent())->selectionModel();
auto sel = sm->selectedIndexes();
bool multi = sel.size() > 1;
if (mod & Qt::Modifier::CTRL) {
} else if (mod & Qt::Modifier::ALT) {
} else {
if (k == Qt::Key_Space) {
if (mod & Qt::Modifier::SHIFT) return dc->cancel(); // TODO: once playback is a thing, shift+space to preview row?
dc->cancel();
SelectionBounds s(sel);
auto* cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
for (int c = s.ch1; c <= s.ch2; c++) {
size_t mpc = 0; // max params in channel
for (int r = 0; r < p->rows; r++) mpc = std::max(mpc, p->rowAt(c, r).numParams());
auto mp = s.maxParamSelected(c);
if (mp < 0) continue;
auto mps = std::min(mpc, static_cast<size_t>(mp));
for (int r = s.y1; r <= s.y2; r++) {
if (multi && mps <= p->rowAt(c, r).numParams()) continue;
auto* dc = new PatternDeltaCommand(p, c, r);
for (size_t i = dc->row.numParams(); i < mps; i++) dc->row.addParam(' ');
if (!multi) dc->row.param(mps) = {' ', 0};
cc->compose(dc);
}
}
return cc->commit("pattern strut");
}
if (multi) {
if (k == Qt::Key_Delete) {
dc->cancel();
SelectionBounds s(sel);
auto* cc = (new CompositeCommand())->reserve((1 + s.y2 - s.y1) * (1 + s.ch2 - s.ch1));
for (int c = s.ch1; c <= s.ch2; c++) {
for (int r = s.y1; r <= s.y2; r++) {
auto* dc = new PatternDeltaCommand(p, c, r);
if (s.portSelected(c)) dc->row.port = -1;
if (s.noteSelected(c)) dc->row.note = -1;
for (int i = static_cast<int>(dc->row.numParams()) - 1; i >= 0; i--) {
if (s.paramSelected(c, i)) dc->row.removeParam(static_cast<size_t>(i));
}
cc->compose(dc);
}
}
return cc->commit("delete selection");
}
// for all other commands, reset selection to cursor and defer
sm->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
}
if (cc == 0) { // port column
if (k == Qt::Key_Delete) {
row.port = -1;
return dc->commit();
}
if (k == Qt::Key_G) { // global commands (tempo and the like)
row.port = -2;
return dc->commit();
}
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.port, i);
return dc->commit();
}
}
} else if (cc == 1) { // note column
if (k == Qt::Key_Delete) {
row.note = -1;
return dc->commit();
}
if (k == Qt::Key_Equal) { // note off
row.note = -2;
return dc->commit();
}
if (k == Qt::Key_Plus) { // shift for hard cut; for some reason this is a separate keycode
row.note = -3;
return dc->commit();
}
for (size_t i = 0; i < (sizeof(pianoKeys) / sizeof(pianoKeys[0])); i++) {
if (k == pianoKeys[i]) { // piano input
row.note = static_cast<int16_t>(i + (12*4)); // C-4
if (mod & Qt::Modifier::SHIFT) row.note += 24; // shift for +2 octave
audioEngine->note = row.note; // TEMP - testing audio engine
if (row.port == -1) { // if no port specified, default to last port used (for a note event) in channel, then (TODO) last port value applied
for (int i = index.row() - 1; i >= 0; i--) {
auto& r = p->rowAt(ch, i);
if (r.port >= 0 && r.note != -1) {
row.port = r.port;
break;
}
}
}
return dc->commit();
} else if (i < 10 && k == numberKeys[i]) { // set octave
if (row.note >= 0) row.note = static_cast<int16_t>((row.note % 12) + 12*i);
audioEngine->note = row.note; // TEMP - testing audio engine
//static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
return dc->commit();
}
}
} else { // param column
size_t par = static_cast<size_t>((cc - (cc % 2)) / 2 - 1);
if (k == Qt::Key_Insert) { // insert from within any place in the param columns
if (row.numParams() >= PatternEditorModel::paramSoftCap) return false; // no overruns
row.insertParam(par, ' ');
auto view = static_cast<PatternEditorView*>(parent());
size_t cpar = row.numParams() - 1;
if (cpar > par) cpar = par;
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(cpar) * 2 + 2));
return dc->commit();
}
if (par < row.numParams()) {
if (k == Qt::Key_Delete) { // remove selected parameter
row.removeParam(par);
if (par >= row.numParams()) { // snap to arrow if beyond
auto view = static_cast<PatternEditorView*>(parent());
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 2));
}
return dc->commit();
}
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return dc->cancel();
if (cc % 2 == 0) { // char column; set to key pressed and move forward
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
row.param(par)[0] = static_cast<unsigned char>(chr);
auto view = static_cast<PatternEditorView*>(parent());
view->setCurrentIndex(index.siblingAtColumn(index.column()+1));
return dc->commit();
} else {
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.param(par)[1], i);
return dc->commit();
}
}
}
} else { // new param; set to key pressed and move forward
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
row.addParam(chr);
auto view = static_cast<PatternEditorView*>(parent());
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 1));
return dc->commit();
}
}
}
// kill command if unused
dc->cancel();
}
return false;
}//*/
QSize PatternEditorItemDelegate::sizeHint(const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) const {
if (index.data().isNull()) return QSize(0, 0);
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
int cc = index.column() % PatternEditorModel::colsPerChannel;
std::string s = "FF";
if (cc == 1) s = "C#2";
else if (cc > 1 && cc % 2 == 0) s = "v";
/*if (index.data().toString() == QString("» ")) {
return fm.boundingRect(QString("»")).size() + QSize(pad*2,0);
}//*/
if (cc > 1) {
return fm.boundingRect(QString::fromStdString(s)).size() + QSize(pad,0); // only one padding on params
}
return fm.boundingRect(QString::fromStdString(s)).size() + QSize(pad*2,0);
//return QSize(24, 16);
}