xybrid/xybrid/ui/patterneditorview.cpp

428 lines
18 KiB
C++

#include "patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include "ui/patterneditormodel.h"
using Xybrid::UI::PatternEditorModel;
#include "ui/patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;
#include "util/strings.h"
#include "util/keys.h"
#include "ui/channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;
#include "data/project.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
#include "editing/compositecommand.h"
#include "editing/patterncommands.h"
using namespace Xybrid::Editing;
#include "audio/audioengine.h"
using namespace Xybrid::Audio;
#include "util/pattern.h"
#include <QKeyEvent>
#include <QShortcut>
#include <QTimer>
#include <QGuiApplication>
#include <QClipboard>
#include <QMimeData>
#include <QCborArray>
#include <QCborMap>
#include <QDebug>
#include <QHeaderView>
#include <QScrollBar>
#include <QHBoxLayout>
#include <QMenu>
#include <QTextEdit>
#include <QInputDialog>
#include <QMessageBox>
namespace {
std::shared_ptr<Pattern> pt = std::make_shared<Pattern>(1, 0); // fallback pattern
}
PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
hdr.reset(new ChannelHeaderView(this));
hdr->setSectionResizeMode(QHeaderView::Fixed);
hdr->setTextElideMode(Qt::ElideNone);//Middle);
hdr->setStretchLastSection(true);
hdr->setSectionsMovable(true);
hdr->setFirstSectionMovable(true);
hdr->setEditTriggers(EditTrigger::DoubleClicked);
// hook up scrolling to update header position
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatternEditorView::updateHeaderOffset);
// and header swap
connect(hdr.get(), &QHeaderView::sectionMoved, this, &PatternEditorView::headerMoved);
// hook up context menu
hdr->setContextMenuPolicy(Qt::CustomContextMenu);
connect(hdr.get(), &QHeaderView::customContextMenuRequested, this, &PatternEditorView::headerContextMenu);
connect(hdr.get(), &QHeaderView::sectionDoubleClicked, this, &PatternEditorView::headerDoubleClicked);
horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);//ResizeToContents);
horizontalHeader()->setStretchLastSection(true);
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
verticalHeader()->setMinimumSectionSize(0);
//verticalHeader()->setStretchLastSection(true);
verticalHeader()->setSectionsClickable(false);
setCornerButtonEnabled(false);
//verticalHeader()->setDefaultAlignment(Qt::AlignTop);
cornerBoxBox.reset(new QWidget(this));
QHBoxLayout *layoutCheckBox = new QHBoxLayout(cornerBoxBox.get());
cornerBox.reset(new QCheckBox());
layoutCheckBox->addWidget(cornerBox.get());
layoutCheckBox->setAlignment(Qt::AlignCenter);
layoutCheckBox->setContentsMargins(0,0,0,0);
cornerBox->setToolTip("Expand to fit names");
cornerBox->setFocusPolicy(Qt::NoFocus);
connect(cornerBox.get(), &QCheckBox::toggled, this, [this](bool state) {
mdl->fitHeaderToName = state;
mdl->updateColumnDisplay();
});
mdl.reset(new PatternEditorModel(this));
del.reset(new PatternEditorItemDelegate(this));
setItemDelegate(&*del);
mdl->setPattern(pt);
setModel(&*mdl);
hdr->setModel(&*mdl->hprox);
// smooth scroll
setVerticalScrollMode(ScrollMode::ScrollPerPixel);
setHorizontalScrollMode(ScrollMode::ScrollPerPixel);
{ /* set up hotkeys */ } {
// transpose notes
auto transpose = [this](int amt, int key = Qt::Key_Alt) {
auto p = mdl->getPattern();
auto sel = this->selectionModel()->selection().first();
auto startCol = sel.left() % Util::colsPerChannel;
if (sel.width() == 1 && startCol != 1) {
if (startCol == 0) { // port
amt = std::clamp(amt, -16, 16);
auto cc = new CompositeCommand();
auto ch = Util::channelForColumn(sel.left());
auto last = sel.bottom();
for (auto i = sel.top(); i <= last; i++) {
if (p->rowAt(ch, i-1).port >= 0) {
auto c = new PatternDeltaCommand(p, ch, i-1);
auto op = c->row.port;
c->row.port = static_cast<int16_t>(std::clamp(op + amt, 0, 255));
if (c->row.port == op) c->cancel();
else cc->compose(c);
}
}
cc->commit("change note port(s)");
} else if (startCol >= 2 && sel.height() == 1) { // single param
amt = std::clamp(amt, -16, 16);
auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top()-1);
auto& r = c->row;
auto par = static_cast<size_t>((sel.left() % Util::colsPerChannel) - 2) / 2;
if (r.numParams() > par) {
if (r.param(par)[0] != ' ') r.param(par)[1] = static_cast<uint8_t>(std::clamp(static_cast<int>(r.param(par)[1]) + amt, 0, 255));
c->commit();
} else c->cancel();
}
} else { // note(s)
amt = std::clamp(amt, -12, 12);
auto cc = new CompositeCommand();
for (auto s : sel.indexes()) { // clazy:exclude=range-loop-detach
if (s.column() % Util::colsPerChannel != 1) continue;
int ch = Util::channelForColumn(s.column());
auto c = new PatternDeltaCommand(p, ch, s.row()-1);
if (c->row.note >= 0) {
c->row.note = static_cast<int16_t>(std::max(c->row.note + amt, 0));
cc->compose(c);
} else c->cancel();
}
cc->commit("transpose note(s)");
startPreview(key);
}
};
connect(new QShortcut(QKeySequence("Alt+Up"), this), &QShortcut::activated, this, [transpose] { transpose(1, Qt::Key_Up); });
connect(new QShortcut(QKeySequence("Alt+Down"), this), &QShortcut::activated, this, [transpose] { transpose(-1, Qt::Key_Down); });
connect(new QShortcut(QKeySequence("Alt+Right"), this), &QShortcut::activated, this, [transpose] { transpose(100, Qt::Key_Right); });
connect(new QShortcut(QKeySequence("Alt+Left"), this), &QShortcut::activated, this, [transpose] { transpose(-100, Qt::Key_Left); });
// fold
connect(new QShortcut(QKeySequence("Ctrl+Space"), this), &QShortcut::activated, this, [this] {
setUpdatesEnabled(false);
mdl->toggleFold();
auto p = mdl->getPattern();
auto ind = currentIndex();
int row = ind.row() - 1;
if (mdl->folded && p->fold > 1 && row % p->fold != 0) {
ind = ind.siblingAtRow(1 + row - (row % p->fold));
setCurrentIndex(ind);
if (selectedIndexes().count() <= 1) selectionModel()->select(ind, QItemSelectionModel::SelectionFlag::ClearAndSelect);
}
scrollTo(ind, ScrollHint::PositionAtCenter);
setUpdatesEnabled(true);
//QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
});
// cut/copy/paste
auto copy = [this] {
auto* clip = QGuiApplication::clipboard();
auto* data = new QMimeData();
auto p = mdl->getPattern();
auto sel = selectionModel()->selection()[0];
auto chMin = Util::channelForColumn(sel.left());
auto chMax = Util::channelForColumn(sel.right());
// cbor data format: (may change between versions, assume a given system only has one Xybrid version installed)
// [ integer first, last, n*[ integer port, note, (param, val)... ]... ]...
QCborArray root;
for (int ch = chMin; ch <= chMax; ch++) {
QCborArray chm;
int first = 0, last = 255;
if (ch == chMin) first = Util::fieldForColumn(sel.left());
if (ch == chMax) last = Util::fieldForColumn(sel.right());
chm << first << last;
for (int r = sel.top(); r <= sel.bottom(); r++) {
QCborArray rm;
auto& row = p->rowAt(ch, r-1);
rm << row.port;
rm << row.note;
if (row.params) for (auto p : *row.params) rm << p[0] << p[1];
chm << rm;
}
root << chm;
}
data->setData("xybrid-internal/x-pattern-copy", root.toCborValue().toCbor());
clip->setMimeData(data);
};
connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Cut), this), &QShortcut::activated, this, [this, copy] {
copy();
this->edit(currentIndex(), EditTrigger::EditKeyPressed, new QKeyEvent(QKeyEvent::Type::KeyPress, Qt::Key_Delete, Qt::KeyboardModifier::NoModifier));
});
connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this), &QShortcut::activated, this, [copy] { copy(); });
connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Paste), this), &QShortcut::activated, this, [this] {
const auto* clip = QGuiApplication::clipboard();
const auto* data = clip->mimeData();
if (!data->hasFormat("xybrid-internal/x-pattern-copy")) return; // no pattern data
auto p = mdl->getPattern();
auto idx = currentIndex();
auto chMin = Util::channelForColumn(idx.column());
auto rMin = idx.row()-1;
auto root = QCborValue::fromCbor(data->data("xybrid-internal/x-pattern-copy")).toArray();
auto cc = new CompositeCommand();
for (int ch = 0; ch < static_cast<int>(p->numChannels()) - chMin && ch < root.size(); ch++) {
auto chm = root[ch].toArray();
int first = static_cast<int>(chm[0].toInteger());
int last = static_cast<int>(chm[1].toInteger());
int pOff = 0;
if (ch == 0 && first >= 2) pOff = std::max(0, Util::fieldForColumn(idx.column()) - first);
for (int r = 0; r < p->rows - rMin && r < chm.size() - 2; r++) {
auto rm = chm[r+2].toArray();
auto c = new PatternDeltaCommand(p, ch+chMin, r+rMin);
auto& row = c->row;
if (first <= 0 && last >= 0) row.port = static_cast<int16_t>(rm[0].toInteger());
if (first <= 1 && last >= 1) row.note = static_cast<int16_t>(rm[1].toInteger());
for (int p = 0; p < (rm.size() - 2) / 2 && p+pOff < Util::paramSoftCap; p++)
if (first <= p+2 && last >= p+2) row.setParam(static_cast<size_t>(p+pOff), static_cast<char>(rm[p*2+2].toInteger()), static_cast<unsigned char>(rm[p*2+3].toInteger()));
cc->compose(c);
}
}
cc->commit("paste pattern data");
});
}
}
PatternEditorView::~PatternEditorView() {
//
/*mdl.release();
del.release();
hdr.release();
cornerBoxBox.release();
cornerBox.release();*/
//horizontalHeader()->deleteLater();
}
void PatternEditorView::keyPressEvent(QKeyEvent* e) {
if (/*event->modifiers() & Qt::Modifier::CTRL &&*/ (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab)) { // don't block ctrl+tab
e->ignore();
return;
//QKeyEvent()
}
if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Insert) {
if (!edit(currentIndex(), AnyKeyPressed, e)) {
e->ignore();
return;
}
}
auto prevInd = currentIndex();
QTableView::keyPressEvent(e);
if (auto ind = currentIndex(); ind != prevInd) scrollTo(ind, ScrollHint::PositionAtCenter); // cursor position changed, scroll to keep up
if (!e->isAutoRepeat()) {
if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
startPreview(Util::unshiftedKey(e->key()));
}
}
}
void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
QAbstractItemView::keyReleaseEvent(e);
if (!e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}
void PatternEditorView::mouseReleaseEvent(QMouseEvent* e) {
QTableView::mouseReleaseEvent(e);
auto sm = selectionModel();
if (sm->selectedIndexes().size() <= 1) {
auto ind = indexAt(e->localPos().toPoint());
if (ind.isValid() && model()->flags(ind) & Qt::ItemFlag::ItemIsEnabled) scrollTo(ind, ScrollHint::PositionAtCenter);
}
}
void PatternEditorView::startPreview(int key) {
auto ind = currentIndex();
int cc = ind.column() % PatternEditorModel::colsPerChannel;
int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
if (cc == 1 || (cc == 0 && key == Qt::Key_Space)) { // note column or space on port
stopPreview(key); // end current preview first, if applicable
auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
auto p = mdl->getPattern()->project->shared_from_this();
previewKey[key] = {r.port, audioEngine->preview(p, r.port, r.note)};
}
}
void PatternEditorView::stopPreview(int key) {
if (auto k = previewKey.find(key); k != previewKey.end()) {
auto p = mdl->getPattern()->project->shared_from_this();
audioEngine->preview(p, k->second.first, -2, k->second.second);
previewKey.erase(k);
}
}
void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {
setUpdatesEnabled(false);
mdl->setPattern(pattern);
//rowCountChanged(0, mdl->rowCount());
auto cur = currentIndex();
auto ind = model()->index(std::clamp(cur.row(), 1, pattern->rows), std::clamp(cur.column(), 0, std::max(0, mdl->columnCount() - 2)));
while (isColumnHidden(ind.column())) ind = ind.siblingAtColumn(ind.column()-1);
while (isRowHidden(ind.row())) ind = ind.siblingAtRow(ind.row()-1);
setCurrentIndex(ind);
selectionModel()->select(ind, QItemSelectionModel::ClearAndSelect);
scrollTo(ind, ScrollHint::PositionAtCenter);
setUpdatesEnabled(true);
}
void PatternEditorView::updateGeometries() {
if (mdl && colUpdateNeeded && horizontalHeader()->count() > 0) {
mdl->updateColumnDisplay();
colUpdateNeeded = false;
} else updateHeader(true); // do this once on every geom update
if (cornerBox) {
cornerBoxBox->setGeometry(verticalHeader()->x(), horizontalHeader()->y(), verticalHeader()->width(), horizontalHeader()->height());
//cornerBox->move(verticalHeader()->x() + verticalHeader()->width() / 2 - cornerBox->minimumWidth() / 2, horizontalHeader()->y() + cornerBox->height() / 2);
}
this->QTableView::updateGeometries();
}
void PatternEditorView::updateHeader(bool full) {
auto* bh = horizontalHeader(); // base header
if (full) {
hdr->reset(); // force section update
for (int i = 0; i < hdr->count(); i++) { // set sizes
constexpr int cpc = PatternEditorModel::colsPerChannel;
int w = 0;
for (int j = 0; j < cpc; j++) {
w += bh->sectionSize(i*cpc+j);
}
hdr->resizeSection(i, w);
}
}
hdr->setGeometry(bh->x(), bh->y(), bh->width(), bh->height());
hdr->setOffset(bh->offset());
}
void PatternEditorView::refresh() {
mdl->refresh();
}
bool PatternEditorView::isFolded() {
return mdl->folded;
}
void PatternEditorView::updateHeaderOffset(int) {
updateHeader(false);
}
void PatternEditorView::headerMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) {
if (logicalIndex == newVisualIndex) return; // assume moving back
hdr->moveSection(newVisualIndex, logicalIndex); // maintain straight-through order
if (logicalIndex >= hdr->count() - 1) return; // no dragging the endcap :|
(new PatternChannelMoveCommand(mdl->getPattern(), oldVisualIndex, newVisualIndex))->commit();
}
void PatternEditorView::headerDoubleClicked(int section) {
startRenameChannel(section);
}
void PatternEditorView::headerContextMenu(QPoint pt) {
int idx = hdr->logicalIndexAt(pt);
std::shared_ptr<Pattern> p = mdl->getPattern();
QMenu* menu = new QMenu(this);
menu->addAction("Add Channel", this, [/*this,*/ idx, p]() {
(new PatternChannelAddCommand(p, idx))->commit();
});
if (idx < hdr->count() - 1) {
menu->addAction("Delete Channel", this, [this, idx, p]() {
if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name), Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
(new PatternChannelDeleteCommand(p, idx))->commit();
});
menu->addAction("Rename Channel...", this, [this, idx, p]() {
if (p != mdl->getPattern()) return; // swapped already
startRenameChannel(idx);
});
}
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(hdr->mapToGlobal(pt));
}
void PatternEditorView::startRenameChannel(int channel) {
auto p = mdl->getPattern();
if (static_cast<size_t>(channel) >= p->numChannels()) return;
auto c = &p->channel(channel);
bool ok = false;
auto capt = QString("Rename channel %1:").arg(channel);
auto n = QInputDialog::getText(this, "Rename...", capt, QLineEdit::Normal, c->name, &ok);
if (!ok) return; // canceled
if (p != mdl->getPattern() || c != &p->channel(channel)) return; // abort if this somehow isn't the channel it was before
(new PatternChannelRenameCommand(p, channel, n))->commit();
}