335 lines
16 KiB
C++
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, Config::colorScheme.patternBg);
|
|
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgMeasure);
|
|
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, Config::colorScheme.patternBgBeat);
|
|
}
|
|
|
|
// selection/cursor highlight
|
|
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, Config::colorScheme.patternSelection);
|
|
if (option.state & QStyle::State_HasFocus) {
|
|
painter->setPen(Config::colorScheme.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, Config::colorScheme.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(Config::colorScheme.patternFgBlank);
|
|
} else {
|
|
if (s == QString(" - ") || s == QString("- ")) painter->setPen(Config::colorScheme.patternFgBlank);
|
|
else if (cc == 0) painter->setPen(Config::colorScheme.patternFgPort);
|
|
else if (cc == 1) painter->setPen(Config::colorScheme.patternFgNote);
|
|
else if (cc % 2 == 0) painter->setPen(Config::colorScheme.patternFgParamCmd);
|
|
else painter->setPen(Config::colorScheme.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);
|
|
}
|