way too much stuff because I kept forgetting to commit
parent
744e9ddaf5
commit
e7a8093b8c
86
notes
86
notes
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
#include "project.h"
|
||||
using Xybrid::Data::Project;
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>&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&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.
|
@ -0,0 +1,5 @@
|
|||
<RCC>
|
||||
<qresource prefix="/fonts">
|
||||
<file>iosevka-term-light.ttf</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -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) {
|
||||
//
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <QHeaderView>
|
||||
|
||||
namespace Xybrid::UI {
|
||||
class ChannelHeaderView : public QHeaderView {
|
||||
public:
|
||||
ChannelHeaderView(QWidget* parent = nullptr);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
//
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue