way too much stuff because I kept forgetting to commit

portability/boost
zetaPRIME 2018-11-28 05:19:10 -05:00
parent 744e9ddaf5
commit e7a8093b8c
18 changed files with 511 additions and 142 deletions

86
notes
View File

@ -15,10 +15,17 @@ project data {
pattern {
name, id
length in rows (duh)
time signature, in beats per measure and rows per beat (can default to project global)
time signature: beats/measure, rows/beat, ticks/row (can default to project global? except that doesn't make complete sense)
tempo change on enter (float, defaults to 0; only applied if >0)
when creating new pattern {
time signature set to project defaults (either static or fallback)
rows = 4 measures
}
per-pattern channels; note continuity is defined by name
- send note-off on entering a pattern without a channel of that name? maybe project setting
^ by default, send note-off on entering a pattern without a channel of that name
also send note-off on old note when triggering a new one regardless of what instrument it is,
*AFTER* sending the new note-on (to not break or make things harder for legato instruments)
command format
01 C-5 v7F ... ... ...
@ -27,27 +34,56 @@ project data {
- leave pitch bends to automation? or build them as per-tick messages from host? also, stepped by tick or smoothed per sample?
x note-on events send the actual note as a float value
- nope, separate event for cents (bcd? that would futz with interpolation though... signed byte, -100..100)
treat port FF as global control?? {
what to do with the notes?
tXX - tempo (second tXX as high byte, .XX for fine tempo (0..100))
> anything else?
}
}
}
TODO {
- have params end off on a + or >
de-hardcode the "» "
- figure out how to autohide empty param columns
- give Row some convenience methods for working with params
implement (top) header span and return channel names
immediate frontburner {
create new Project and hook that up to the main window instead of just making an empty pattern
( Project::new(), Pattern::new(Project*) )
make and hook up UI for displaying all existing patterns by index, and for displaying the sequence
implement drag and drop for pattern swapping/rearrangement (std::rotate)
add miscellaneous editables (artist, song title, project bpm; pattern name, length etc.)
- make pattern editor detect ctrl and alt modifiers
make everything relevant check if editing is locked
- figure out what library to use for messagepack
implement saving (probably a FileIO helper class)
hook up new/open/save menu items
}
- subclass QTableView and integrate with model
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
- implement (top) header span and return channel names
double click to rename channel
QHeaderView::setSectionsMovable(bool movable)
QHeaderView::sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
^ need to re-reposition the thing?
QHeaderView is a QAbstractItemView as of qt5, so can set an item delegate
- editing, duh
multiselect editing (at least delete)
pattern background colors (and time signature, duh)
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
project type {
how to bind to mainwindow?
}
at some point {
undo
multiselect editing (at least delete)
de-hardcode pattern editor colors
}
}
use signals/slots to signal project/pattern updates etc.
resampler object {
one used internally for each note
reference to sample
@ -55,8 +91,38 @@ resampler object {
double position (number of samples with fractional part)
}
gain slider/knob gadget: dBFS [10^(x / 20)]
scale from -60 to +6 in increments of .1, with linear fade to zero below -60
sine panning http://folk.ntnu.no/oyvinbra/delete/Lesson1Panning.html
though for vXX, it's perfectly sufficient to use mult^4
JS example of sine panning, -1.0 to +1.0 {
const m = 1.0/Math.cos(Math.PI*.25)
function f(x) {
var s = (x+1) * Math.PI * .25
return (Math.cos(s)*m) + " " + (Math.sin(s)*m)
}
}
lv2 support: lilv (duh) for actual plugin loading, suil for UI embedding
eventually hook luajit up with TLSF allocator to aid in real time (maybe look at nedmalloc too)
(requires a patch to lib_aux.c, so static link/straight include?)
https://github.com/OpenMusicKontrollers/Tjost/blob/master/LuaJIT-2.0.3-rt.patch
to static link https://stackoverflow.com/a/30235934
graph+node+port system {
dependencies are a straight ordering, worked backwards from the mix output on port hookup change (node local)
this way, we don't bother processing anything that doesn't actually contribute to output in any way
before playback starts, playback thread assembles an expanded queue (graph has a function that recursively expands queue given a ref to a pre-reserved vector)
can use the same logic to count active nodes (make sure to include the graph i/o ports unless implementing those another way? actually yeah, explicit pull operation)
... how will the worker threads tell when they've outpaced the queue, and how will they wait properly?
something something ready flags (all input nodes and containing graph; main graph and command ports always have the bit set)
graphs are also nodes
}
keybinds {
pattern editor {
note column {

View File

@ -8,6 +8,7 @@
#include <string>
namespace Xybrid::Data {
class Project;
class Pattern {
public:
class Row { // with std::unique_ptr<std::vector>, each Row is 12 bytes inline on 64-bit (8 bytes on 32)
@ -72,6 +73,12 @@ namespace Xybrid::Data {
static Row fallbackRow;
public:
// raw pointer is fine for now, since a project will never be destroyed without explicitly orphaning patterns
// (and probably deleting them since basically the only reason one would be kept alive is if it's open in the pattern editor,
// which would then immediately update with a pattern from opening a new project, or the window would close)
Project* project;
std::string name;
int rows = 64;

2
xybrid/data/project.cpp Normal file
View File

@ -0,0 +1,2 @@
#include "project.h"
using Xybrid::Data::Project;

31
xybrid/data/project.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <stdint.h>
#include <memory>
#include <list>
#include <vector>
#include <string>
#include "data/pattern.h"
namespace Xybrid::Data {
class Project {
public:
bool editingLocked = false;
std::string title;
std::string artist;
size_t sampleRate = 48000; // global sr for rendering
float tempo = 140.0;
// default time signature
// shared to ease reordering and prevent crashes due to invalidating things that UI stuff is using
std::vector<std::shared_ptr<Pattern>> patterns;
std::vector<Pattern*> sequence; // nullptr as separator
//std::shared_ptr<Graph> mainGraph;
// list of input nodes is just part of mainGraph
};
}

View File

@ -1,15 +1,19 @@
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>
#include <vector>
#include <QDebug>
#include <QFontDatabase>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
qDebug() << QString::number(sizeof(std::vector<unsigned short>));
// make sure bundled fonts are loaded
QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf");
Xybrid::MainWindow w;
w.show();
return a.exec();
}

View File

@ -1,8 +1,10 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
using Xybrid::MainWindow;
#include <QToolButton>
#include <QPushButton>
#include <QDebug>
#include <QKeyEvent>
#include <QTabWidget>
#include "ui/patterneditoritemdelegate.h"
@ -18,14 +20,13 @@ MainWindow::MainWindow(QWidget *parent) :
ui(new Ui::MainWindow) {
ui->setupUi(this);
QTabWidget* t = this->ui->tabWidget;
//QToolButton* mb = t->findChild<QToolButton*>();
auto t = this->ui->tabWidget;
//QToolButton* menuBtn = new QToolButton(this);
//menuBtn->setArrowType(Qt::ArrowType::DownArrow);
t->setCornerWidget(this->ui->menuBar);
t->setCornerWidget(this->ui->label, Qt::TopLeftCorner);
auto mb = this->ui->menuBar;
mb->setStyleSheet("QMenuBar { background: transparent; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
mb->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
//mb->setSizePolicy(QSizePolicy(QSizePolicy::Policy::Maximum, QSizePolicy::Policy::Maximum));
//mb->resize(mb->size().width(), t->tabBar()->size().height());
@ -36,18 +37,26 @@ MainWindow::MainWindow(QWidget *parent) :
pattern->channels[1].rows[0].port = 1;
pattern->channels[1].rows[0].note = 0;
//t->tabBar()->setTabText(0, QString::number(pattern->rows));
auto pe = t->findChild<Xybrid::UI::PatternEditorView*>("patternEditor");
//pe->installEventFilter(dlg);
//pe->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
//pe->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
//pe->setStyleSheet("QTableView::item { border: 0px; margin: 0px; padding: 0px; }");
//pe->setStyleSheet("QTableView::item { text-overflow: clip; overflow: hidden; white-space: nowrap; }");
pe->setPattern(&*pattern);
}
MainWindow::~MainWindow() {
delete ui;
}
bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
if (event->type() == QEvent::KeyPress) {
auto ke = static_cast<QKeyEvent*>(event);
qDebug() << QString("key pressed: %1").arg((ke->key()));
if (ke->key() == Qt::Key_Tab) {
qDebug() << QString("tab");
auto* t = this->ui->tabWidget;
t->setTabPosition(QTabWidget::TabPosition::South);
t->setCurrentIndex(t->currentIndex() + 1);
return true;
}
}
return false;
}

View File

@ -9,15 +9,20 @@ namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow {
Q_OBJECT
namespace Xybrid {
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
private:
Ui::MainWindow* ui;
std::unique_ptr<Xybrid::Data::Pattern> pattern; // temporary pattern for testing the editor
std::unique_ptr<Xybrid::UI::PatternEditorModel> pmodel;
};
private:
Ui::MainWindow* ui;
std::unique_ptr<Data::Pattern> pattern; // temporary pattern for testing the editor
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
};
}

View File

@ -29,9 +29,15 @@
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="pattern">
<attribute name="title">
<string>Pattern</string>
@ -98,6 +104,19 @@
<attribute name="title">
<string>Patchboard</string>
</attribute>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>310</x>
<y>80</y>
<width>61</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>(logo here)</string>
</property>
</widget>
</widget>
</widget>
</item>
@ -122,7 +141,10 @@
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionpickle"/>
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSave_As"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
@ -131,17 +153,56 @@
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
<string>He&amp;lp</string>
</property>
</widget>
<addaction name="menua_menu_item"/>
<addaction name="menuEdit"/>
<addaction name="menuHelp"/>
</widget>
<action name="actionpickle">
<action name="actionOpen">
<property name="text">
<string>Open</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
<property name="shortcutVisibleInContextMenu">
<bool>true</bool>
</property>
</action>
<action name="actionNew">
<property name="text">
<string>New</string>
</property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
<property name="shortcutVisibleInContextMenu">
<bool>true</bool>
</property>
</action>
<action name="actionSave">
<property name="text">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
<property name="shortcutVisibleInContextMenu">
<bool>true</bool>
</property>
</action>
<action name="actionSave_As">
<property name="text">
<string>Save As...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
<property name="shortcutVisibleInContextMenu">
<bool>true</bool>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
@ -153,5 +214,22 @@
</customwidget>
</customwidgets>
<resources/>
<connections/>
<connections>
<connection>
<sender>actionOpen</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>showMaximized()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>459</x>
<y>305</y>
</hint>
</hints>
</connection>
</connections>
</ui>

Binary file not shown.

5
xybrid/res/resources.qrc Normal file
View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/fonts">
<file>iosevka-term-light.ttf</file>
</qresource>
</RCC>

View File

@ -0,0 +1,10 @@
#include "channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;
#include "ui/patterneditormodel.h"
#include <QDebug>
ChannelHeaderView::ChannelHeaderView(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) {
//
}

View File

@ -0,0 +1,10 @@
#pragma once
#include <QHeaderView>
namespace Xybrid::UI {
class ChannelHeaderView : public QHeaderView {
public:
ChannelHeaderView(QWidget* parent = nullptr);
};
}

View File

@ -11,11 +11,12 @@ using Xybrid::UI::PatternEditorModel;
#include <QKeyEvent>
namespace {
constexpr int pad = 2;
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,
@ -64,115 +65,119 @@ bool PatternEditorItemDelegate::eventFilter(QObject *obj, QEvent *event) {
bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option [[maybe_unused]], const QModelIndex &index) {
auto type = event->type();
if (type == QEvent::KeyPress || type == QEvent::KeyRelease) {
if (type == QEvent::KeyPress) {
auto k = static_cast<QKeyEvent*>(event)->key(); // grab key
auto mod = static_cast<QKeyEvent*>(event)->modifiers();
// treat delete/backspace keyup as a press because QAbstractItemView explicitly discards the keydown for some stupid reason
if (type == QEvent::KeyRelease && (k == Qt::Key_Delete || k == Qt::Key_Backspace)) type = QEvent::KeyPress;
//if (type == QEvent::KeyRelease) return false; // TEMP - early exit here
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& row = p.rowAt(ch, index.row());
if (cc == 0) { // port column
if (k == Qt::Key_Delete) {
row.port = -1;
return true;
}
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.port, i);
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
if (mod & Qt::Modifier::CTRL) {
} else if (mod & Qt::Modifier::ALT) {
} else {
if (cc == 0) { // port column
if (k == Qt::Key_Delete) {
row.port = -1;
return true;
}
}
} else if (cc == 1) { // note column
if (k == Qt::Key_Delete) {
row.note = -1;
return true;
}
if (k == Qt::Key_Z) { // note off
row.note = -2;
return true;
}
if (k == Qt::Key_X) { // hard cut
row.note = -3;
return true;
}
for (size_t i = 0; i < (sizeof(pianoKeys) / sizeof(int)); i++) {
if (k == pianoKeys[i]) { // piano input
row.note = static_cast<int16_t>(i + (12*4) + 3); // C-4
if (mod & Qt::Modifier::SHIFT) row.note += 24; // shift for +2 octave
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;
auto ind = index.siblingAtColumn(index.column() - 1);
emit model->dataChanged(ind, ind, {Qt::DisplayRole});
break;
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.port, i);
return true;
}
}
} else if (cc == 1) { // note column
if (k == Qt::Key_Delete) {
row.note = -1;
return true;
}
if (k == Qt::Key_Equal) { // note off
row.note = -2;
return true;
}
if (k == Qt::Key_Plus) { // shift for hard cut; for some reason this is a separate keycode
row.note = -3;
return true;
}
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
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;
auto ind = index.siblingAtColumn(index.column() - 1);
emit model->dataChanged(ind, ind, {Qt::DisplayRole});
break;
}
}
}
return true;
} else if (i < 10 && k == numberKeys[i]) { // set octave
if (row.note >= 0) row.note = static_cast<int16_t>((row.note % 12) + 12*i);
static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
return true;
}
return true;
} else if (i < 10 && k == numberKeys[i]) { // set octave
if (row.note >= 0) row.note = static_cast<int16_t>((row.note % 12) + 12*i);
static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
return true;
}
}
} 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);
// update whole row (phew!)
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
m->updateColumnDisplay(); // update column autohide
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 true;
}
if (par < row.numParams()) {
if (k == Qt::Key_Delete) { // remove selected parameter
row.removeParam(par);
} 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);
// update whole row (phew!)
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
m->updateColumnDisplay(); // update column autohide
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));
}
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 true;
}
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
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 true;
} else {
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.param(par)[1], i);
return true;
if (par < row.numParams()) {
if (k == Qt::Key_Delete) { // remove selected parameter
row.removeParam(par);
// update whole row (phew!)
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
m->updateColumnDisplay(); // update column autohide
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 true;
}
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
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 true;
} else {
for (size_t i = 0; i < 16; i++) {
if (k == numberKeys[i]) {
insertDigit(row.param(par)[1], i);
return true;
}
}
}
} 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);
// update whole row (phew!)
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
m->updateColumnDisplay(); // update column autohide
auto view = static_cast<PatternEditorView*>(parent());
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 1));
return true;
}
} 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);
// update whole row (phew!)
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
m->updateColumnDisplay(); // update column autohide
auto view = static_cast<PatternEditorView*>(parent());
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 1));
return true;
}
}
//

View File

@ -1,18 +1,25 @@
#include "patterneditormodel.h"
using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorHeaderProxyModel;
using Xybrid::Data::Pattern;
#include "ui/patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include <QDebug>
#include <QString>
#include <QFontMetrics>
namespace { // helper functions
int cellWidthBase = -1;
int cellWidthParam;
int cellWidthParamTab;
int headerHeight;
constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
constexpr char notemap[] = "A-A#B-C-C#D-D#E-F-F#G-G#";
constexpr char notemap[] = "C-C#D-D#E-F-F#G-G#A-A#B-";
std::string hexStr(unsigned char *data, uint len) {
std::string s(len * 2, ' ');
@ -38,8 +45,23 @@ namespace { // helper functions
}
}
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.reset(new PatternEditorHeaderProxyModel(parent, this));
}
int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
@ -80,8 +102,19 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
}
QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::DisplayRole) {
//if (orientation == Qt::Orientation::Horizontal) return QString(""); // avoid stretch
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
if (orientation == Qt::Orientation::Horizontal) {
int cc = section % colsPerChannel;
int ch = (section - cc) / colsPerChannel;
if (cc == 0) {
auto n = QString::fromStdString(pattern->channels.at(static_cast<size_t>(ch)).name);
if (n.length() == 0) return QString("(Channel %1)").arg(ch);
return n;
}
return QString(""); // those aren't even supposed to be there
}
if (role == Qt::ToolTipRole) return QString(""); // no tool tip for simple numbers
return QString::number(section);
} else if (role == Qt::SizeHintRole) {
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
@ -91,6 +124,37 @@ QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation
return QVariant();
}
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 (static_cast<size_t>(section) >= pattern->channels.size()) return QString("");
auto n = QString::fromStdString(pattern->channels.at(static_cast<size_t>(section)).name);
if (n.length() == 0) return QString("(Channel %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.width(QString("FF")) + fm.width(QString("C#2")) + cellPadding*4;
cellWidthParamTab = fm.width(QString("v")) + cellPadding;
cellWidthParam = cellWidthParamTab + fm.width(QString("FF")) + cellPadding;
}/*
auto& c = pattern->channels.at(static_cast<size_t>(section));
size_t maxParams = 0;
for (auto& r : c.rows) {
if (r.numParams() > maxParams) maxParams = r.numParams();
}
int width = cellWidthBase;
width += static_cast<int>(maxParams) * cellWidthParam;
if (maxParams < paramSoftCap) width += cellWidthParamTab;
return QSize(width, headerHeight);*/
return QSize(0, headerHeight);
}
return QVariant();
}
void PatternEditorModel::setPattern(Pattern* pattern) {
this->pattern = pattern;
//updateColumnDisplay();
@ -111,4 +175,5 @@ void PatternEditorModel::updateColumnDisplay() {
view->setColumnHidden(static_cast<int>(ch*PatternEditorModel::colsPerChannel+i), i >= 3+2*maxParams);
}
}
view->updateHeader(true);
}

View File

@ -5,26 +5,42 @@
#include "data/pattern.h"
namespace Xybrid::UI {
class PatternEditorHeaderProxyModel;
class PatternEditorModel : public QAbstractTableModel {
Q_OBJECT
Xybrid::Data::Pattern* pattern;
Data::Pattern* pattern;
public:
static constexpr int paramSoftCap = 16; // maximum number of parameter columns that can be displayed per channel; the rest are hidden
static constexpr int colsPerChannel = 2 + (2 * paramSoftCap);
static constexpr int cellPadding = 2;
std::unique_ptr<PatternEditorHeaderProxyModel> hprox;
PatternEditorModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override ;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int hdrColumnCount() const;
QVariant hdrData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
void setPattern(Xybrid::Data::Pattern* pattern);
Xybrid::Data::Pattern& getPattern() {
void setPattern(Data::Pattern* pattern);
Data::Pattern& getPattern() {
return *pattern;
}
void updateColumnDisplay();
};
class PatternEditorHeaderProxyModel : public QAbstractTableModel {
PatternEditorModel* pm;
public:
PatternEditorHeaderProxyModel(QObject *parent, PatternEditorModel* p);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
};
}

View File

@ -5,6 +5,9 @@ using Xybrid::UI::PatternEditorModel;
#include "ui/patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;
#include "ui/channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;
#include "data/pattern.h"
using Xybrid::Data::Pattern;
@ -12,26 +15,48 @@ using Xybrid::Data::Pattern;
#include <QDebug>
#include <QHeaderView>
#include <QScrollBar>
namespace {
Pattern pt(1, 0);
}
PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
hdr.reset(new ChannelHeaderView(this));
hdr->setSectionResizeMode(QHeaderView::Fixed);//ResizeToContents);
hdr->setTextElideMode(Qt::ElideMiddle);
hdr->setStretchLastSection(true);
// hook up scrolling to update header position
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatternEditorView::updateHeaderOffset);
horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
//horizontalHeader()->setSectionsMovable(true);
//horizontalHeader()->moveSection(1, 3);
mdl.reset(new PatternEditorModel(this));
del.reset(new PatternEditorItemDelegate(this));
setItemDelegate(&*del);
mdl->setPattern(&pt);
setModel(&*mdl);
hdr->setModel(&*mdl->hprox);
//horizontalHeader()->setModel(&*mdl);
}
PatternEditorView::~PatternEditorView() {
//
horizontalHeader()->deleteLater();
}
void PatternEditorView::keyPressEvent(QKeyEvent *event) {
if (/*event->modifiers() & Qt::Modifier::CTRL &&*/ (event->key() == Qt::Key_Tab || event->key() == Qt::Key_Backtab)) { // don't block ctrl+tab
event->ignore();
return;
//QKeyEvent()
}
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace || event->key() == Qt::Key_Insert) {
if (!edit(currentIndex(), AnyKeyPressed, event)) {
event->ignore();
@ -51,6 +76,26 @@ void PatternEditorView::updateGeometries() {
if (mdl && colUpdateNeeded && horizontalHeader()->count() > 0) {
mdl->updateColumnDisplay();
colUpdateNeeded = false;
}
} else updateHeader(true); // do this once on every geom update
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::updateHeaderOffset(int) {
updateHeader(false);
}

View File

@ -5,6 +5,7 @@
#include <QTableView>
#include "data/pattern.h"
#include "ui/channelheaderview.h"
/*#include "ui/patterneditormodel.h"
#include "ui/patterneditoritemdelegate.h"*/
@ -16,6 +17,7 @@ namespace Xybrid::UI {
std::unique_ptr<PatternEditorModel> mdl;
std::unique_ptr<PatternEditorItemDelegate> del;
std::unique_ptr<ChannelHeaderView> hdr;
bool colUpdateNeeded = false;
@ -24,9 +26,11 @@ namespace Xybrid::UI {
~PatternEditorView() override;
void keyPressEvent(QKeyEvent* event) override;
void setPattern(Xybrid::Data::Pattern* pattern);
void setPattern(Data::Pattern* pattern);
void updateGeometries() override;
void updateHeader(bool full = false);
void updateHeaderOffset(int);
void keyboardSearch(const QString&) override {} // disable accidental search

View File

@ -32,14 +32,18 @@ SOURCES += \
data/pattern.cpp \
ui/patterneditormodel.cpp \
ui/patterneditoritemdelegate.cpp \
ui/patterneditorview.cpp
ui/patterneditorview.cpp \
data/project.cpp \
ui/channelheaderview.cpp
HEADERS += \
mainwindow.h \
data/pattern.h \
ui/patterneditormodel.h \
ui/patterneditoritemdelegate.h \
ui/patterneditorview.h
ui/patterneditorview.h \
data/project.h \
ui/channelheaderview.h
FORMS += \
mainwindow.ui
@ -48,3 +52,6 @@ FORMS += \
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
RESOURCES += \
res/resources.qrc