246 lines
9.8 KiB
C++
246 lines
9.8 KiB
C++
#include "patterneditormodel.h"
|
|
using Xybrid::UI::PatternEditorModel;
|
|
using Xybrid::UI::PatternEditorHeaderProxyModel;
|
|
using Xybrid::Data::Pattern;
|
|
#include "ui/patterneditorview.h"
|
|
using Xybrid::UI::PatternEditorView;
|
|
|
|
#include "util/strings.h"
|
|
|
|
#include <QDebug>
|
|
#include <QString>
|
|
#include <QFontMetrics>
|
|
#include <QTimer>
|
|
#include <QScrollBar>
|
|
|
|
namespace { // helper functions
|
|
int cellWidthBase = -1;
|
|
int cellWidthParam;
|
|
int cellWidthParamTab;
|
|
int headerHeight;
|
|
QSize hsz, hhsz;
|
|
|
|
constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
|
|
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
|
|
};
|
|
|
|
constexpr char notemap[] = "C-C#D-D#E-F-F#G-G#A-A#B-";
|
|
|
|
QString hexStr(unsigned char *data, int len) {
|
|
QString s(len * 2, ' ');
|
|
for (int i = 0; i < len; ++i) {
|
|
s[2 * i] = hexmap[(data[i] & 0xF0) >> 4];
|
|
s[2 * i + 1] = hexmap[data[i] & 0x0F];
|
|
}
|
|
return s;
|
|
}
|
|
QString byteStr(int t) {
|
|
unsigned char c = static_cast<unsigned char>(t & 255);
|
|
return hexStr(&c, 1);
|
|
}
|
|
|
|
QString noteStr(int n) {
|
|
QString s(3, ' ');
|
|
int nn = n % 12;
|
|
int oc = (n - nn) / 12;
|
|
s[2] = '0' + static_cast<char>(oc);
|
|
s[0] = notemap[nn*2];
|
|
s[1] = notemap[nn*2+1];
|
|
return s;
|
|
}
|
|
}
|
|
|
|
PatternEditorHeaderProxyModel::PatternEditorHeaderProxyModel(QObject *parent, PatternEditorModel* p) : QAbstractTableModel(parent), pm(p) { }
|
|
int PatternEditorHeaderProxyModel::rowCount(const QModelIndex&) const {
|
|
return 1;
|
|
}
|
|
int PatternEditorHeaderProxyModel::columnCount(const QModelIndex&) const {
|
|
return pm->hdrColumnCount();
|
|
}
|
|
QVariant PatternEditorHeaderProxyModel::data(const QModelIndex&, int) const {
|
|
return QVariant();
|
|
}
|
|
QVariant PatternEditorHeaderProxyModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
return pm->hdrData(section, orientation, role);
|
|
}
|
|
|
|
PatternEditorModel::PatternEditorModel(QObject *parent)
|
|
:QAbstractTableModel(parent) {
|
|
hprox = new PatternEditorHeaderProxyModel(parent, this);
|
|
|
|
auto view = static_cast<PatternEditorView*>(parent);
|
|
int rowHeight = [] {
|
|
QFontMetrics fm = QFontMetrics(QFont());
|
|
return (fm.boundingRect("255").size() + QSize(8, 4)).height();
|
|
}();
|
|
endHeight = (view->viewport()->height() - rowHeight) / 2;
|
|
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this, view, rowHeight] {
|
|
endHeight = (view->viewport()->height() - rowHeight) / 2;
|
|
emit this->layoutChanged();
|
|
});
|
|
}
|
|
|
|
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
|
|
//if (pattern->channels.size() == 0) return 1;
|
|
return pattern->rows + 2;
|
|
}
|
|
|
|
int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
|
|
if (pattern->channels.size() == 0) return 1;
|
|
return colsPerChannel * static_cast<int>(pattern->channels.size()) + 1;
|
|
}
|
|
|
|
QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
|
|
if (role == Qt::DisplayRole) {
|
|
if (index.row() == 0) return QVariant();
|
|
if (index.row() > pattern->rows || index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
|
|
int cc = index.column() % colsPerChannel;
|
|
int ch = (index.column() - cc) / colsPerChannel;
|
|
auto& row = pattern->rowAt(ch, index.row()-1);
|
|
if (cc == 0) { // port
|
|
if (row.port >= 0 && row.port < 256) return byteStr(row.port);
|
|
if (row.port == -2) return qs("(G)");
|
|
if (row.port == -3) return qs("L");
|
|
return qs(" - ");
|
|
} else if (cc == 1) { // note
|
|
if (row.note >= 0) return noteStr(row.note);
|
|
if (row.note == -2) return qs(" ^ "); // note off
|
|
if (row.note == -3) return qs(" x "); // hard cut
|
|
return qs(" - ");
|
|
} else {
|
|
size_t cp = static_cast<size_t>(((cc - 2) - (cc % 2)) / 2);
|
|
if (cc % 2 == 0) {
|
|
if (row.numParams() > cp) return QString(1,static_cast<char>(row.params->at(cp)[0]));
|
|
if (row.numParams() == cp) return qs("» ");
|
|
return qs("");
|
|
}
|
|
if (row.numParams() > cp) {
|
|
if (row.params->at(cp)[0] == ' ') return qs("- ");
|
|
return byteStr(row.params->at(cp)[1]);
|
|
}
|
|
return qs("");
|
|
}
|
|
} else if (role == Qt::SizeHintRole) {
|
|
if (index.row() >= pattern->rows) return QSize(-1, -1);
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
if (role == Qt::DisplayRole) {
|
|
if (orientation == Qt::Orientation::Horizontal) return QVariant(); // blank actual-header
|
|
if (section == 0 || section > pattern->rows) return QVariant(); // blank end section
|
|
return QString::number(section-1);
|
|
} else if (role == Qt::SizeHintRole) {
|
|
if (orientation == Qt::Orientation::Vertical) if (section == 0 || section > pattern->rows) return QSize(-1, endHeight); // fill ends to center
|
|
if (hsz.isEmpty()) {
|
|
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
|
|
hsz = fm.boundingRect("255").size() + QSize(8, 4); // this should fit 0-999
|
|
hhsz = QSize(0, hsz.height());
|
|
}
|
|
return orientation == Qt::Vertical ? hsz : hhsz;
|
|
|
|
//return QSize(0, fm.height() + 4);
|
|
} else if (role == Qt::TextAlignmentRole) return Qt::AlignCenter;
|
|
return QVariant();
|
|
}
|
|
|
|
Qt::ItemFlags PatternEditorModel::flags(const QModelIndex &index) const {
|
|
if (index.row() == 0 || index.row() >= pattern->rows + 1 || (pattern->channels.size() > 0 && index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size()))) {
|
|
return QAbstractTableModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
}
|
|
return QAbstractTableModel::flags(index);
|
|
}
|
|
|
|
int PatternEditorModel::hdrColumnCount() const {
|
|
return static_cast<int>(pattern->channels.size()) + 1;
|
|
}
|
|
QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) const {
|
|
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
|
if (pattern->channels.size() == 0) return QString("(no channels)");
|
|
if (static_cast<size_t>(section) >= pattern->channels.size()) return QString("");
|
|
auto n = pattern->channels.at(static_cast<size_t>(section)).name;
|
|
//if (n.length() == 0) return QString("(Channel %1)").arg(section);
|
|
if (n.length() == 0) return QString("(ch%1)").arg(section);
|
|
return n;
|
|
} else if (role == Qt::SizeHintRole) {
|
|
if (cellWidthBase <= 0) {
|
|
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
|
headerHeight = fm.height()+4;
|
|
cellWidthBase = fm.horizontalAdvance(QString("FF")) + fm.horizontalAdvance(QString("C#2")) + cellPadding*4;
|
|
cellWidthParamTab = fm.horizontalAdvance(QString("v")) + cellPadding;
|
|
cellWidthParam = cellWidthParamTab + fm.horizontalAdvance(QString("FF")) + cellPadding;
|
|
}
|
|
return QSize(0, headerHeight);
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
void PatternEditorModel::setPattern(const std::shared_ptr<Pattern>& pattern) {
|
|
if (this->pattern == pattern) return;
|
|
this->pattern = pattern;
|
|
refresh();
|
|
}
|
|
|
|
void PatternEditorModel::updateColumnDisplay() {
|
|
if (pattern == nullptr) return;
|
|
auto view = static_cast<PatternEditorView*>(parent());
|
|
//view->setUpdatesEnabled(false);
|
|
//auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
|
|
for (size_t ch = 0; ch < pattern->channels.size(); ch++) {
|
|
auto& c = pattern->channels.at(ch);
|
|
size_t maxParams = 0;
|
|
for (auto& r : c.rows) {
|
|
if (r.numParams() > maxParams) maxParams = r.numParams();
|
|
}
|
|
int lastShown = 0;
|
|
int chWidth = 0;
|
|
for (size_t i = 0; i < PatternEditorModel::colsPerChannel; i++) {
|
|
auto col = static_cast<int>(ch*PatternEditorModel::colsPerChannel+i);
|
|
view->setColumnHidden(col, false); // always unhide to ensure hiding actually prevents columns from being selected
|
|
if (i < 3+2*maxParams) {
|
|
view->resizeColumnToContents(col);
|
|
lastShown = col;
|
|
chWidth += view->columnWidth(col);
|
|
} else view->setColumnHidden(col, true);
|
|
//view->setColumnWidth(col, 1);
|
|
}
|
|
int minWidth = 0;
|
|
if (fitHeaderToName) { // ensure exact fit
|
|
QStyleOptionHeader opt;
|
|
opt.initFrom(view->horizontalHeader());
|
|
opt.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal;
|
|
opt.orientation = Qt::Horizontal;
|
|
opt.section = 0;
|
|
opt.fontMetrics = view->horizontalHeader()->fontMetrics();
|
|
opt.text = c.name;
|
|
minWidth = view->horizontalHeader()->style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), view->horizontalHeader()).width();
|
|
}
|
|
int lsw = view->columnWidth(lastShown);
|
|
view->setColumnWidth(lastShown, std::max(lsw + 3, minWidth - (chWidth - lsw)));
|
|
}
|
|
//view->setUpdatesEnabled(true);
|
|
view->updateHeader(true);
|
|
}
|
|
|
|
void PatternEditorModel::updateFold() {
|
|
auto view = static_cast<PatternEditorView*>(parent());
|
|
//view->setUpdatesEnabled(false);
|
|
int ifold = 1;
|
|
if (folded && pattern->fold > 1) ifold = pattern->fold;
|
|
int rows = this->rowCount()-2;
|
|
view->setRowHidden(0, false);
|
|
view->setRowHidden(rows+1, false);
|
|
for (int i = 0; i < rows; i++) {
|
|
view->setRowHidden(i+1, false); // dispel any "phantoms" we might end up having
|
|
if (i < pattern->rows && i % ifold != 0) view->setRowHidden(i+1, true);
|
|
}
|
|
//view->setUpdatesEnabled(true);
|
|
}
|
|
|
|
void PatternEditorModel::toggleFold() {
|
|
folded = !folded;
|
|
|
|
refresh();
|
|
}
|