so so much UI stuff (full multi pattern and sequence editing!)

portability/boost
zetaPRIME 2018-12-04 17:04:45 -05:00
parent d8af8f463b
commit 701218936b
23 changed files with 826 additions and 68 deletions

26
notes
View File

@ -1,7 +1,8 @@
IMPORTANT LINKS {
https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
lowpass filter https://www.embeddedrelated.com/showarticle/779.php
https://github.com/ThePhD/sol2
https://github.com/cameron314/concurrentqueue
}
project data {
@ -53,37 +54,36 @@ TODO {
- add
- delete
toggle box for expand-to-name
- toggle box for expand-to-name
- create new Project and hook that up to the main window instead of just making an empty pattern
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)
- 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)
- make list drag immediately update the sequencer as well
- add context menus for pattern list and sequencer
add miscellaneous editables (artist, song title, project bpm; pattern name, length etc.)
make splitter collapse update pattern editor
- make splitter collapse update pattern editor
- make pattern editor detect ctrl and alt modifiers
make everything relevant check if editing is locked
- hook up new/open/save menu items
- figure out what library to use for messagepack (official msgpack-c)
implement saving (probably a FileIO helper class)
- hook up new/open/save menu items
}
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
pattern background colors (and time signature, duh)
- pattern background colors (and time signature, duh)
- pattern editor selection highlight is broken on some qt themes (most notably Breeze)! (FIXED)
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
- de-hardcode pattern editor colors
}
}

27
save format Normal file
View File

@ -0,0 +1,27 @@
msgpack
project: [
"xybrid:project"
(uint32 version)
{
"meta": { "artist": ... "title": ... "comment": ... etc. }
"patterns": [ array of pattern structs ]
"sequence": [ array of pattern numbers, uint32, separator is MAX_VALUE ]
}
]
pattern: {
"name": "asdf"
"rows": 64 // actually necessary?
"channels": [ {
"name": "asdf"
"rows": [ // probably better name...
[ int16 port, int16 note, n*(uint8 c, uint8 amt) ] // for each row
]
} ]
}

View File

@ -0,0 +1,4 @@
#include "colorscheme.h"
using Xybrid::Config::ColorScheme;
ColorScheme ColorScheme::current;

View File

@ -0,0 +1,23 @@
#pragma once
#include <QColor>
namespace Xybrid::Config {
class ColorScheme {
public:
static ColorScheme current;
ColorScheme() = default;
QColor patternSelection = QColor(127, 63, 255, 63);
QColor patternBg = QColor(23, 23, 23);
QColor patternBgBeat = QColor(31, 31, 31);
QColor patternBgMeasure = QColor(39, 39, 39);
QColor patternFgBlank = QColor(127, 127, 127);
QColor patternFgPort = QColor(191, 191, 191);
QColor patternFgNote = QColor(255, 255, 255);
QColor patternFgParamCmd = QColor(191,163,255);
QColor patternFgParamAmt = QColor(191,222,255);
};
}

View File

@ -3,14 +3,27 @@ using Xybrid::Data::Pattern;
using Row = Pattern::Row;
using Channel = Pattern::Channel;
#include "data/project.h"
Row Pattern::fallbackRow(-1337, -1337);
Channel Pattern::fallbackChannel(0);
std::array<unsigned char, 2> Row::fallbackParam {'.', 0};
Row::Row(const Row& o) noexcept {
*this = o;
}
Row& Row::operator=(const Row& o) noexcept {
port = o.port;
note = o.note;
// copy-constructor the underlying vector
if (o.params) params.reset(new std::vector<std::array<unsigned char, 2>>(*o.params));
return *this;
}
Channel::Channel(int numRows, std::string name) : Channel() {
this->name = name;
this->rows.resize(static_cast<size_t>(numRows));
rows.resize(static_cast<size_t>(numRows));
}
Pattern::Pattern() {
@ -18,9 +31,8 @@ Pattern::Pattern() {
}
Pattern::Pattern(int rows, int channels) : Pattern() {
this->rows = rows;
for (int i = 0; i < channels; i++) this->channels.emplace_back();
this->setLength(rows);
setLength(rows);
}
void Pattern::setLength(int r) {
@ -41,6 +53,16 @@ void Pattern::deleteChannel(int at) {
channels.erase(channels.begin() + at);
}
bool Pattern::valid() const {
return (project && index < project->patterns.size() && this == project->patterns[index].get());
}
bool Pattern::validFor(const Project* p) const {
return valid() && project == p;
}
bool Pattern::validFor(const std::shared_ptr<Project>& p) const {
return valid() && project == p.get();
}
Channel& Pattern::channel(int c) {
auto cc = static_cast<size_t>(c);
if (cc >= channels.size()) return fallbackChannel;

View File

@ -10,6 +10,17 @@
namespace Xybrid::Data {
class Project;
struct TimeSignature {
int beatsPerMeasure = 4;
int rowsPerBeat = 4;
int ticksPerRow = 6;
TimeSignature() = default;
TimeSignature(int b, int r, int t) : beatsPerMeasure(b), rowsPerBeat(r), ticksPerRow(t) {}
constexpr int rowsPerMeasure() const {
return beatsPerMeasure * rowsPerBeat;
}
};
class Pattern {
public:
class Row { // with std::unique_ptr<std::vector>, each Row is 12 bytes inline on 64-bit (8 bytes on 32)
@ -21,11 +32,15 @@ namespace Xybrid::Data {
std::unique_ptr<std::vector<std::array<unsigned char, 2>>> params = nullptr; // empty by default
Row() = default;
Row(const Row&) noexcept;
Row(Row&&) = default;
Row(int16_t p, int16_t n) : Row() {
port = p;
note = n;
}
Row& operator=(const Row&) noexcept;
size_t numParams() const {
if (!this->params) return 0;
return this->params->size();
@ -68,6 +83,7 @@ namespace Xybrid::Data {
std::vector<Row> rows;
Channel() = default;
Channel(const Channel&) = default;
Channel(int numRows, std::string name = "");
};
private:
@ -84,20 +100,27 @@ namespace Xybrid::Data {
std::string name;
int rows = 64;
float tempo = 0; // don't set playback tempo
TimeSignature time;
std::vector<Channel> channels;
Pattern();
Pattern(int rows, int channels);
Pattern(const Pattern&) = default;
Pattern(int rows, int channels = 0);
void setLength(int rows);
void addChannel(int at = -1);
void deleteChannel(int at);
size_t numChannels() {
size_t numChannels() const {
return channels.size();
}
bool valid() const;
bool validFor(const Project*) const;
bool validFor(const std::shared_ptr<Project>&) const;
Channel& channel(int channel);
Row& rowAt(int channel, int row);
};

View File

@ -11,10 +11,27 @@ void Project::updatePatternIndices() {
for (size_t i = 0; i < patterns.size(); i++) patterns[i]->index = i;
}
std::shared_ptr<Pattern> Project::newPattern() {
auto pt = std::make_shared<Pattern>();
std::shared_ptr<Pattern> Project::newPattern(size_t idx) {
auto pt = std::make_shared<Pattern>(time.rowsPerMeasure() * 4);
pt->time = time;
pt->project = this;
pt->index = patterns.size();
patterns.push_back(pt);
if (idx >= patterns.size()) {
pt->index = patterns.size();
patterns.push_back(pt);
} else {
patterns.insert(patterns.begin() + static_cast<ptrdiff_t>(idx), pt);
updatePatternIndices();
}
return pt;
}
void Project::removePattern(Pattern* p) {
if (!p || p->project != this || p->index >= patterns.size() || patterns[p->index].get() != p) return;
// remove from sequence first
sequence.erase(std::remove(sequence.begin(), sequence.end(), p), sequence.end());
// remove from pattern list and adjust numbers
patterns.erase(patterns.begin() + static_cast<ptrdiff_t>(p->index));
updatePatternIndices();
// finally, explicitly orphan
p->project = nullptr;
}

View File

@ -29,6 +29,7 @@ namespace Xybrid::Data {
size_t sampleRate = 48000; // global sr for rendering
float tempo = 140.0;
TimeSignature time;
// default time signature
// shared to ease reordering and prevent crashes due to invalidating things that UI stuff is using
@ -43,6 +44,8 @@ namespace Xybrid::Data {
void updatePatternIndices();
std::shared_ptr<Pattern> newPattern();
std::shared_ptr<Pattern> newPattern(size_t index = static_cast<size_t>(-1));
void removePattern(Pattern*);
};
}

View File

@ -4,14 +4,23 @@ using Xybrid::MainWindow;
#include <QDebug>
#include <QKeyEvent>
#include <QShortcut>
#include <QTabWidget>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include "util/strings.h"
#include "ui/patternlistmodel.h"
#include "ui/patternsequencermodel.h"
#include "ui/patterneditoritemdelegate.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
using Xybrid::UI::PatternListModel;
using Xybrid::UI::PatternSequencerModel;
using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorItemDelegate;
@ -24,21 +33,171 @@ MainWindow::MainWindow(QWidget *parent) :
ui(new Ui::MainWindow) {
ui->setupUi(this);
auto t = this->ui->tabWidget;
auto t = ui->tabWidget;
t->setCornerWidget(this->ui->menuBar);
t->setCornerWidget(this->ui->label, Qt::TopLeftCorner);
auto mb = this->ui->menuBar;
t->setCornerWidget(ui->menuBar);
t->setCornerWidget(ui->label, Qt::TopLeftCorner);
auto mb = ui->menuBar;
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());
// prevent right pane of pattern view from being collapsed
ui->patternViewSplitter->setCollapsible(1, false);
connect(ui->patternViewSplitter, &QSplitter::splitterMoved, [this](int, int) {
// and when the list is collapsed, make sure header size is updated
ui->patternEditor->updateHeader();
});
{ /* Set up pattern list */ } {
// model
ui->patternList->setModel(new PatternListModel(ui->patternList, this));
// events
// on selection change
connect(ui->patternList->selectionModel(), &QItemSelectionModel::currentChanged, [this](const QModelIndex& index, const QModelIndex& old) {
if (index == old) return; // no actual change
size_t idx = static_cast<size_t>(index.row());
if (idx >= project->patterns.size()) return;
this->selectPatternForEditing(project->patterns[idx].get());
});
// on click
connect(ui->patternList, &QListView::clicked, [this]() { // deselect on sequencer when list clicked
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, -1));
});
// rightclick menu
connect(ui->patternList, &QListView::customContextMenuRequested, [this](const QPoint& pt) {
size_t idx = static_cast<size_t>(ui->patternList->indexAt(pt).row());
std::shared_ptr<Pattern> p = nullptr;
if (idx < project->patterns.size()) p = project->patterns[idx];
QMenu* menu = new QMenu(this);
menu->addAction("New Pattern", [this, idx]() {
auto np = project->newPattern(idx);
updatePatternLists();
selectPatternForEditing(np.get());
});
if (p) {
menu->addAction("Duplicate Pattern", [this, p, idx]() {
auto np = project->newPattern(idx + 1);
*np = *p;
project->updatePatternIndices();
updatePatternLists();
selectPatternForEditing(np.get());
});
menu->addSeparator();
menu->addAction("Delete Pattern", [this, p]() {
if (QMessageBox::warning(this, "Are you sure?", QString("Remove pattern %1?").arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
project->removePattern(p.get());
updatePatternLists();
});
}
menu->popup(ui->patternList->mapToGlobal(pt));
});
}
{ /* Set up sequencer */ } {
// model
ui->patternSequencer->setModel(new PatternSequencerModel(ui->patternSequencer, this));
// some metrics that the designer doesn't seem to like doing
ui->patternSequencer->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
ui->patternSequencer->horizontalHeader()->setDefaultSectionSize(24);
ui->patternSequencer->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
ui->patternSequencer->verticalHeader()->setDefaultSectionSize(24);
// events
// on selection change
connect(ui->patternSequencer->selectionModel(), &QItemSelectionModel::currentChanged, [this](const QModelIndex& index, const QModelIndex&) {
size_t idx = static_cast<size_t>(index.column());
if (idx >= project->sequence.size()) return;
this->selectPatternForEditing(project->sequence[idx]);
});
// rightclick menu
connect(ui->patternSequencer, &QTableView::customContextMenuRequested, [this](const QPoint& pt) {
size_t idx = static_cast<size_t>(ui->patternSequencer->indexAt(pt).column());
/*std::shared_ptr<Pattern> p = nullptr;
if (idx < project->sequence.size()) {
Pattern* pr = project->sequence[idx];
}*/
QMenu* menu = new QMenu(this);
menu->addAction("Insert Pattern", [this, idx]() {
if (!editingPattern->validFor(project)) return; // nope
int si = static_cast<int>(std::min(idx, project->sequence.size()));
project->sequence.insert(project->sequence.begin() + si, editingPattern.get());
updatePatternLists();
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, si+1));
});
menu->addAction("Insert Separator", [this, idx]() {
int si = static_cast<int>(std::min(idx, project->sequence.size()));
project->sequence.insert(project->sequence.begin() + si, nullptr);
updatePatternLists();
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, si+1));
});
if (idx < project->sequence.size()) menu->addAction("Remove", [this, idx]() {
project->sequence.erase(project->sequence.begin() + static_cast<ptrdiff_t>(idx));
updatePatternLists();
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, static_cast<int>(idx)-1));
});
menu->addSeparator();
menu->addAction("Create New Pattern", [this, idx]() {
auto np = project->newPattern();
int si = static_cast<int>(std::min(idx, project->sequence.size()));
project->sequence.insert(project->sequence.begin() + si, np.get());
updatePatternLists();
selectPatternForEditing(np.get());
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, si));
});
if (idx < project->sequence.size() && project->sequence[idx]) {
menu->addAction("Duplicate Pattern", [this, idx, p = project->patterns[project->sequence[idx]->index]]() {
auto np = project->newPattern(p->index + 1);
*np = *p;
project->updatePatternIndices();
int si = static_cast<int>(std::min(idx + 1, project->sequence.size()));
project->sequence.insert(project->sequence.begin() + si, np.get());
updatePatternLists();
selectPatternForEditing(np.get());
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, si));
});
}
menu->popup(ui->patternSequencer->mapToGlobal(pt));
});
}
{ /* Set up keyboard shortcuts for pattern view */ } {
// Ctrl+PgUp/Down - previous or next pattern in sequencer
connect(new QShortcut(QKeySequence("Ctrl+PgUp"), ui->pattern), &QShortcut::activated, [this]() {
auto i = ui->patternSequencer->currentIndex();
if (!i.isValid()) {
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(ui->patternSequencer->horizontalHeader()->count() - 1, 0));
return;
}
auto count = ui->patternSequencer->horizontalHeader()->count();
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() - 1) % count));
});
connect(new QShortcut(QKeySequence("Ctrl+PgDown"), ui->pattern), &QShortcut::activated, [this]() {
auto i = ui->patternSequencer->currentIndex();
if (!i.isValid()) {
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, 0));
return;
}
auto count = ui->patternSequencer->horizontalHeader()->count();
ui->patternSequencer->setCurrentIndex(i.siblingAtColumn((count + i.column() + 1) % count));
});
}
// Set up signaling from project to UI
socket.reset(new UISocket());
connect(socket.get(), &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
// and start with a new project
menuFileNew();
// TEMP: fill out some initial data
project->sequence.push_back(nullptr);
project->patterns[0]->name = "waffle iron";
project->sequence.push_back(project->newPattern().get());
}
MainWindow::~MainWindow() {
@ -63,7 +222,7 @@ bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
void MainWindow::menuFileNew() {
auto hold = project; // keep alive until done
project = std::make_shared<Project>();
project->newPattern();
project->sequence.push_back(project->newPattern().get());
onNewProjectLoaded();
}
@ -90,10 +249,19 @@ void MainWindow::onNewProjectLoaded() {
pt = p;
break;
}
updatePatternLists();
selectPatternForEditing(pt);
}
void MainWindow::updatePatternLists() {
emit ui->patternList->model()->layoutChanged();
emit ui->patternSequencer->model()->layoutChanged();
if (editingPattern && !editingPattern->validFor(project)) // if current pattern invalidated, select new one
selectPatternForEditing(project->patterns[std::min(editingPattern->index, project->patterns.size() - 1)].get());
}
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
if (!pattern || pattern == editingPattern.get()) return false; // no u
if (pattern->project != project.get()) return false; // wrong project
if (project->patterns.size() <= pattern->index) return false; // invalid id
auto sp = project->patterns[pattern->index];
@ -101,7 +269,8 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) {
auto hold = editingPattern; // keep alive until done
editingPattern = sp;
ui->patternEditor->setPattern(editingPattern.get());
ui->patternEditor->setPattern(editingPattern);
ui->patternList->setCurrentIndex(ui->patternList->model()->index(static_cast<int>(editingPattern->index), 0));
return true;
}

View File

@ -25,8 +25,14 @@ namespace Xybrid {
std::shared_ptr<Data::Pattern> editingPattern; // temporary pattern for testing the editor
void onNewProjectLoaded();
void updatePatternLists();
bool selectPatternForEditing(Data::Pattern*);
public:
Data::Project* getProject() const {
return project.get();
}
protected:
bool eventFilter(QObject *obj, QEvent *event) override;

View File

@ -63,7 +63,10 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QListView" name="listView">
<property name="handleWidth">
<number>2</number>
</property>
<widget class="QListView" name="patternList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>1</horstretch>
@ -76,6 +79,18 @@
<height>0</height>
</size>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
</widget>
<widget class="QWidget" name="patternViewPane" native="true">
<property name="sizePolicy">
@ -92,7 +107,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
@ -106,6 +121,64 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableView" name="patternSequencer">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
<property name="font">
<font>
<family>Iosevka Term Light</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>24</number>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>24</number>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="Xybrid::UI::PatternEditorView" name="patternEditor">
<property name="font">

View File

@ -1,6 +1,9 @@
#include "patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;
#include "config/colorscheme.h"
using Xybrid::Config::ColorScheme;
#include "ui/patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include "ui/patterneditormodel.h"
@ -31,6 +34,24 @@ namespace {
}
void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
{ /* background */ } {
auto p = const_cast<PatternEditorModel*>(static_cast<const PatternEditorModel*>(index.model()))->getPattern();
painter->fillRect(option.rect, ColorScheme::current.patternBg);
if (index.row() % p->time.rowsPerMeasure() == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgMeasure);
else if (index.row() % p->time.rowsPerBeat == 0) painter->fillRect(option.rect, ColorScheme::current.patternBgBeat);
}
// selection/cursor highlight
if (option.state & QStyle::State_Selected) painter->fillRect(option.rect, ColorScheme::current.patternSelection);
if (option.state & QStyle::State_HasFocus) {
painter->setPen(ColorScheme::current.patternSelection);
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(0,0,-1,-1));
painter->drawRect(option.rect.adjusted(1,1,-2,-2));
//painter->fillRect(option.rect, ColorScheme::current.patternSelection);
}
// and main data
QString s = index.data().toString();
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
int cc = index.column() % PatternEditorModel::colsPerChannel;
@ -41,13 +62,13 @@ void PatternEditorItemDelegate::paint(QPainter *painter, const QStyleOptionViewI
}
if (s == QString("» ")) {
align = Qt::AlignVCenter | Qt::AlignLeft;
painter->setPen(QColor(127,127,127));
painter->setPen(ColorScheme::current.patternFgBlank);
} else {
if (s == QString(" - ")) painter->setPen(QColor(127,127,127));
else if (cc == 0) painter->setPen(QColor(191,191,191));
else if (cc == 1) painter->setPen(QColor(255,255,255));
else if (cc % 2 == 0) painter->setPen(QColor(191,163,255));
else painter->setPen(QColor(191,222,255));
if (s == QString(" - ")) painter->setPen(ColorScheme::current.patternFgBlank);
else if (cc == 0) painter->setPen(ColorScheme::current.patternFgPort);
else if (cc == 1) painter->setPen(ColorScheme::current.patternFgNote);
else if (cc % 2 == 0) painter->setPen(ColorScheme::current.patternFgParamCmd);
else painter->setPen(ColorScheme::current.patternFgParamAmt);
}
painter->drawText(option.rect, align, s);
}
@ -70,10 +91,10 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
auto k = static_cast<QKeyEvent*>(event)->key(); // grab key
auto mod = static_cast<QKeyEvent*>(event)->modifiers();
auto m = static_cast<PatternEditorModel*>(model); // we know this will always be pattern editor
auto& p = m->getPattern();
auto p = m->getPattern();
int cc = index.column() % PatternEditorModel::colsPerChannel;
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
auto& row = p.rowAt(ch, index.row());
auto& row = p->rowAt(ch, index.row());
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
if (mod & Qt::Modifier::CTRL) {
@ -111,7 +132,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
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);
auto& r = p->rowAt(ch, i);
if (r.port >= 0 && r.note != -1) {
row.port = r.port;
auto ind = index.siblingAtColumn(index.column() - 1);

View File

@ -71,11 +71,11 @@ int PatternEditorModel::rowCount(const QModelIndex & /*parent*/) const {
int PatternEditorModel::columnCount(const QModelIndex & /*parent*/) const {
if (pattern->channels.size() == 0) return 1;
return colsPerChannel * static_cast<int>(pattern->channels.size());
return colsPerChannel * static_cast<int>(pattern->channels.size()) + 1;
}
QVariant PatternEditorModel::data(const QModelIndex &index, int role) const {
if (pattern->channels.size() == 0) return QVariant();
if (index.column() >= colsPerChannel * static_cast<int>(pattern->channels.size())) return QVariant();
if (role == Qt::DisplayRole) {
int cc = index.column() % colsPerChannel;
int ch = (index.column() - cc) / colsPerChannel;
@ -109,13 +109,20 @@ QVariant PatternEditorModel::headerData(int section, Qt::Orientation orientation
if (orientation == Qt::Orientation::Horizontal) return QVariant(); // blank actual-header
return QString::number(section);
} else if (role == Qt::SizeHintRole) {
auto fm = QFontMetrics(QFont("Iosevka Term Light", 9));
auto fm = QFontMetrics(QFont(/*"Iosevka Term Light", 9*/));
if (orientation == Qt::Orientation::Vertical) return fm.boundingRect("127").size() + QSize(4, 4);
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.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;
}
@ -149,7 +156,8 @@ QVariant PatternEditorModel::hdrData(int section, Qt::Orientation, int role) con
return QVariant();
}
void PatternEditorModel::setPattern(Pattern* pattern) {
void PatternEditorModel::setPattern(const std::shared_ptr<Pattern>& pattern) {
if (this->pattern == pattern) return;
this->pattern = pattern;
refresh();
}
@ -178,7 +186,17 @@ void PatternEditorModel::updateColumnDisplay() {
} else view->setColumnHidden(col, true);
//view->setColumnWidth(col, 1);
}
int minWidth = std::min(100, fm.width(QString::fromStdString(c.name)) + 8);
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 = QString::fromStdString(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 File

@ -9,7 +9,7 @@ namespace Xybrid::UI {
class PatternEditorModel : public QAbstractTableModel {
Q_OBJECT
Data::Pattern* pattern;
std::shared_ptr<Data::Pattern> pattern;
public:
static constexpr int paramSoftCap = 16; // maximum number of parameter columns that can be displayed per channel; the rest are hidden
@ -18,17 +18,21 @@ namespace Xybrid::UI {
std::unique_ptr<PatternEditorHeaderProxyModel> hprox;
bool fitHeaderToName = false;
PatternEditorModel(QObject *parent);
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;
Qt::ItemFlags flags(const QModelIndex & index) const override;
int hdrColumnCount() const;
QVariant hdrData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
void setPattern(Data::Pattern* pattern);
Data::Pattern& getPattern() {
return *pattern;
void setPattern(const std::shared_ptr<Data::Pattern>& pattern);
std::shared_ptr<Data::Pattern> getPattern() {
return pattern;
}
void updateColumnDisplay();

View File

@ -5,10 +5,13 @@ using Xybrid::UI::PatternEditorModel;
#include "ui/patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;
#include "util/strings.h"
#include "ui/channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;
#include "data/pattern.h"
#include "data/project.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
#include <QKeyEvent>
@ -16,13 +19,15 @@ using Xybrid::Data::Pattern;
#include <QHeaderView>
#include <QScrollBar>
#include <QHBoxLayout>
#include <QMenu>
#include <QTextEdit>
#include <QInputDialog>
#include <QMessageBox>
namespace {
Pattern pt(1, 0);
std::shared_ptr<Pattern> pt = std::make_shared<Pattern>(1, 0); // fallback pattern
}
PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
@ -46,16 +51,30 @@ PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
connect(hdr.get(), &QHeaderView::sectionDoubleClicked, this, &PatternEditorView::headerDoubleClicked);
horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);//ResizeToContents);
horizontalHeader()->setStretchLastSection(true);
verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
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](bool state) {
mdl->fitHeaderToName = state;
mdl->updateColumnDisplay();
});
mdl.reset(new PatternEditorModel(this));
del.reset(new PatternEditorItemDelegate(this));
setItemDelegate(&*del);
mdl->setPattern(&pt);
mdl->setPattern(pt);
setModel(&*mdl);
hdr->setModel(&*mdl->hprox);
}
@ -80,7 +99,7 @@ void PatternEditorView::keyPressEvent(QKeyEvent *event) {
QAbstractItemView::keyPressEvent(event);
}
void PatternEditorView::setPattern(Xybrid::Data::Pattern *pattern) {
void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {
mdl->setPattern(pattern);
}
@ -89,6 +108,10 @@ void PatternEditorView::updateGeometries() {
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();
}
@ -122,7 +145,7 @@ void PatternEditorView::headerMoved(int logicalIndex, int oldVisualIndex, int ne
int nf = 0;
if (newVisualIndex > oldVisualIndex) nf = min + 1;
else nf = max;
auto& chn = mdl->getPattern().channels;
auto& chn = mdl->getPattern()->channels;
std::rotate(chn.begin() + min, chn.begin() + nf, chn.begin() + max + 1);
this->dataChanged( // update everything
mdl->index(0, 0, QModelIndex()),
@ -140,18 +163,21 @@ void PatternEditorView::headerDoubleClicked(int 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", [idx, this]() {
mdl->getPattern().addChannel(idx);
QMenu* menu = new QMenu(this);
menu->addAction("Add Channel", [this, idx, p]() {
p->addChannel(idx);
mdl->refresh();
});
if (idx < hdr->count() - 1) {
menu->addAction("Delete Channel", [idx, this]() {
mdl->getPattern().deleteChannel(idx);
menu->addAction("Delete Channel", [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)).arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
p->deleteChannel(idx);
mdl->refresh();
});
menu->addAction("Rename Channel", [idx, this]() {
menu->addAction("Rename Channel", [this, idx, p]() {
if (p != mdl->getPattern()) return; // swapped already
startRenameChannel(idx);
});
}
@ -160,14 +186,14 @@ void PatternEditorView::headerContextMenu(QPoint pt) {
}
void PatternEditorView::startRenameChannel(int channel) {
auto p = &mdl->getPattern();
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, QString::fromStdString(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
if (p != mdl->getPattern() || c != &p->channel(channel)) return; // abort if this somehow isn't the channel it was before
c->name = n.toStdString(); // and set name
mdl->updateColumnDisplay(); // update sizes and such
}

View File

@ -2,6 +2,7 @@
#include <memory>
#include <QCheckBox>
#include <QTableView>
#include "data/pattern.h"
@ -18,6 +19,8 @@ namespace Xybrid::UI {
std::unique_ptr<PatternEditorModel> mdl;
std::unique_ptr<PatternEditorItemDelegate> del;
std::unique_ptr<ChannelHeaderView> hdr;
std::unique_ptr<QWidget> cornerBoxBox;
std::unique_ptr<QCheckBox> cornerBox;
bool colUpdateNeeded = false;
@ -29,7 +32,7 @@ namespace Xybrid::UI {
void keyPressEvent(QKeyEvent* event) override;
void keyboardSearch(const QString&) override {} // disable accidental search
void setPattern(Data::Pattern* pattern);
void setPattern(const std::shared_ptr<Data::Pattern>& pattern);
void updateHeader(bool full = false);

View File

@ -0,0 +1,107 @@
#include "patternlistmodel.h"
using Xybrid::UI::PatternListModel;
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
#include <QDebug>
#include <QMimeData>
#include "mainwindow.h"
PatternListModel::PatternListModel(QObject *parent, MainWindow* window) : QAbstractListModel (parent) {
this->window = window;
}
int PatternListModel::rowCount(const QModelIndex &parent [[maybe_unused]]) const {
auto* project = window->getProject();
if (!project) return 0;
return static_cast<int>(project->patterns.size());
}
int PatternListModel::columnCount(const QModelIndex &parent [[maybe_unused]]) const {
return 1;
}
QVariant PatternListModel::data(const QModelIndex &index, int role) const {
auto* project = window->getProject();
if (!project) return QVariant();
if (role == Qt::DisplayRole) {
auto pattern = project->patterns[static_cast<size_t>(index.row())];
return QString("%1: %2").arg(pattern->index, 1, 10, QChar('0')).arg(QString::fromStdString(pattern->name.empty() ? "(unnamed)" : pattern->name));
}
if (role == Qt::EditRole) {
return QString::fromStdString(project->patterns[static_cast<size_t>(index.row())]->name);
}
return QVariant();
}
bool PatternListModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (role == Qt::EditRole) {
auto* project = window->getProject();
if (!project) return true;
auto pattern = project->patterns[static_cast<size_t>(index.row())];
pattern->name = value.toString().toStdString();
}
return true;
}
Qt::ItemFlags PatternListModel::flags(const QModelIndex &index) const {
return Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | QAbstractListModel::flags(index);
}
Qt::DropActions PatternListModel::supportedDropActions() const {
return Qt::CopyAction | Qt::MoveAction;
}
QStringList PatternListModel::mimeTypes() const {
QStringList types;
types << "xybrid-internal/x-pattern-index";
return types;
}
QMimeData *PatternListModel::mimeData(const QModelIndexList &indexes) const {
auto d = new QMimeData();
QByteArray dd;
QDataStream stream(&dd, QIODevice::WriteOnly);
size_t idx = static_cast<size_t>(indexes[0].row());
Project* prj = window->getProject();
if (!prj) return d; // if somehow nullptr, just return a blank
stream.writeRawData(reinterpret_cast<char*>(&idx), sizeof(size_t));
stream.writeRawData(reinterpret_cast<char*>(&prj), sizeof(void*));
d->setData("xybrid-internal/x-pattern-index", dd);
return d;
}
bool PatternListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column [[maybe_unused]], const QModelIndex &parent [[maybe_unused]]) {
if (!data->hasFormat("xybrid-internal/x-pattern-index")) return false;
if (action == Qt::IgnoreAction) return true; // can accept type
std::shared_ptr<Pattern> p;
{
QByteArray dd = data->data("xybrid-internal/x-pattern-index");
QDataStream stream(&dd, QIODevice::ReadOnly);
size_t idx;
Project* prj;
stream.readRawData(reinterpret_cast<char*>(&idx), sizeof(size_t));
stream.readRawData(reinterpret_cast<char*>(&prj), sizeof(void*));
if (prj != window->getProject()) return false; // wrong or invalid project
if (idx >= prj->patterns.size()) return false; // index out of range
p = prj->patterns[idx];
}
if (parent.isValid()) { // if dropped onto an item and not between, place on opposite side
row = parent.row();
if (row > static_cast<int>(p->index)) row += 1;
}
if (row < 0) row = static_cast<int>(p->project->patterns.size()); // if dropped on empty space, snap to end
if (row > static_cast<int>(p->index)) row -= 1; // compensate ahead of time for snap-out
p->project->patterns.erase(p->project->patterns.begin() + static_cast<int>(p->index));
p->project->patterns.insert(p->project->patterns.begin() + row, p);
p->project->updatePatternIndices();
emit p->project->socket->updatePatternLists();
return true;
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <QAbstractListModel>
#include "data/project.h"
namespace Xybrid {
class MainWindow;
}
namespace Xybrid::UI {
class PatternListModel : public QAbstractListModel {
Q_OBJECT
// raw pointer because the model's lifetime is dependent on the window
MainWindow* window;
public:
PatternListModel(QObject* parent, MainWindow* window);
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;
bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex & index) const override;
Qt::DropActions supportedDropActions() const override;
QStringList mimeTypes() const override;
QMimeData* mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
};
}

View File

@ -0,0 +1,135 @@
#include "patternsequencermodel.h"
using Xybrid::UI::PatternSequencerModel;
#include <QMimeData>
using Xybrid::Data::Pattern;
using Xybrid::Data::Project;
#include "mainwindow.h"
PatternSequencerModel::PatternSequencerModel(QObject *parent, MainWindow* window) : QAbstractTableModel (parent) {
this->window = window;
}
int PatternSequencerModel::rowCount(const QModelIndex &parent [[maybe_unused]]) const {
return 1;
}
int PatternSequencerModel::columnCount(const QModelIndex &parent [[maybe_unused]]) const {
auto* project = window->getProject();
if (!project) return 0;
return static_cast<int>(window->getProject()->sequence.size() + 1);
}
QVariant PatternSequencerModel::data(const QModelIndex &index, int role) const {
auto* project = window->getProject();
if (!project) return QVariant();
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
bool toolTip = (role == Qt::ToolTipRole);
if (static_cast<size_t>(index.column()) >= project->sequence.size())
return !toolTip ? QString("+") : QString("Add new");
auto* pattern = project->sequence[static_cast<size_t>(index.column())];
if (!pattern) return !toolTip ? QString("-") : QString("(separator)");
if (!toolTip) return QString("%1").arg(pattern->index, 1, 10, QChar('0'));
if (pattern->name.empty()) return QVariant(); // no tool tip without name
return QString("(%1) %2").arg(pattern->index, 1, 10, QChar('0')).arg(QString::fromStdString(pattern->name));
}
if (role == Qt::TextAlignmentRole ) return Qt::AlignHCenter + Qt::AlignVCenter;
return QVariant();
}
Qt::ItemFlags PatternSequencerModel::flags(const QModelIndex &index) const {
return Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | QAbstractTableModel::flags(index);
}
Qt::DropActions PatternSequencerModel::supportedDropActions() const {
return Qt::CopyAction | Qt::MoveAction;
}
QStringList PatternSequencerModel::mimeTypes() const {
QStringList types;
types << "xybrid-internal/x-pattern-index";
types << "xybrid-internal/x-sequence-index";
return types;
}
QMimeData* PatternSequencerModel::mimeData(const QModelIndexList &indexes) const {
auto d = new QMimeData();
QByteArray dd;
QDataStream stream(&dd, QIODevice::WriteOnly);
size_t idx = static_cast<size_t>(indexes[0].column());
Project* prj = window->getProject();
if (!prj) return d; // if somehow nullptr, just return a blank
stream.writeRawData(reinterpret_cast<char*>(&idx), sizeof(size_t));
stream.writeRawData(reinterpret_cast<char*>(&prj), sizeof(void*));
d->setData("xybrid-internal/x-sequence-index", dd);
return d;
}
bool PatternSequencerModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row [[maybe_unused]], int column, const QModelIndex &parent [[maybe_unused]]) {
if (data->hasFormat("xybrid-internal/x-pattern-index")) {
if (action == Qt::IgnoreAction) return true; // can accept type
Pattern* p;
{
QByteArray dd = data->data("xybrid-internal/x-pattern-index");
QDataStream stream(&dd, QIODevice::ReadOnly);
size_t idx;
Project* prj;
stream.readRawData(reinterpret_cast<char*>(&idx), sizeof(size_t));
stream.readRawData(reinterpret_cast<char*>(&prj), sizeof(void*));
if (prj != window->getProject()) return false; // wrong or invalid project
if (idx >= prj->patterns.size()) return false; // index out of range
p = prj->patterns[idx].get();
}
if (parent.isValid()) { // if dropped onto an item and not between, place on opposite side
column = parent.column();
if (column > static_cast<int>(p->index)) column += 1;
}
if (column < 0 || column > static_cast<int>(p->project->sequence.size())) column = static_cast<int>(p->project->sequence.size()); // if dropped on empty space, snap to end
p->project->sequence.insert(p->project->sequence.begin() + column, p);
emit layoutChanged();
return true;
}
if (data->hasFormat("xybrid-internal/x-sequence-index")) {
if (action == Qt::IgnoreAction) return true; // can accept type
bool copy = (action == Qt::CopyAction);
size_t idx;
Project* prj;
{
QByteArray dd = data->data("xybrid-internal/x-sequence-index");
QDataStream stream(&dd, QIODevice::ReadOnly);
stream.readRawData(reinterpret_cast<char*>(&idx), sizeof(size_t));
stream.readRawData(reinterpret_cast<char*>(&prj), sizeof(void*));
if (prj != window->getProject()) return false; // wrong or invalid project
if (idx >= prj->sequence.size()) return false; // index out of range
}
if (parent.isValid()) { // if dropped onto an item and not between, place on opposite side
column = parent.column();
if (column > static_cast<int>(idx)) column += 1;
}
if (column < 0 || column > static_cast<int>(prj->sequence.size())) column = static_cast<int>(prj->sequence.size()); // if dropped on empty space, snap to end
if (!copy && column > static_cast<int>(idx)) column -= 1; // compensate ahead of time for snap-out
Pattern* p = prj->sequence[idx];
if (!copy) prj->sequence.erase(prj->sequence.begin() + static_cast<int>(idx));
prj->sequence.insert(prj->sequence.begin() + column, p);
prj->updatePatternIndices();
emit layoutChanged();
return true;
}
return false;
}

View File

@ -0,0 +1,30 @@
#pragma once
#include <QAbstractTableModel>
#include "data/project.h"
namespace Xybrid {
class MainWindow;
}
namespace Xybrid::UI {
class PatternSequencerModel : public QAbstractTableModel {
Q_OBJECT
// raw pointer because the model's lifetime is dependent on the window
MainWindow* window;
public:
PatternSequencerModel(QObject* parent, MainWindow* window);
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;
Qt::ItemFlags flags(const QModelIndex & index) const override;
Qt::DropActions supportedDropActions() const override;
QStringList mimeTypes() const override;
QMimeData* mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
};
}

View File

@ -6,6 +6,6 @@ namespace Xybrid {
class UISocket : public QObject {
Q_OBJECT
signals:
//
void updatePatternLists();
};
}

9
xybrid/util/strings.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <QString>
namespace Xybrid::Util {
template<typename Num> inline QString numAndName(Num num, const std::string& name) {
if (name.empty()) return QString::number(num);
return QString("%1 (\"%2\")").arg(num).arg(QString::fromStdString(name));
}
}

View File

@ -34,7 +34,10 @@ SOURCES += \
ui/patterneditoritemdelegate.cpp \
ui/patterneditorview.cpp \
data/project.cpp \
ui/channelheaderview.cpp
ui/channelheaderview.cpp \
ui/patternsequencermodel.cpp \
ui/patternlistmodel.cpp \
config/colorscheme.cpp
HEADERS += \
mainwindow.h \
@ -44,7 +47,11 @@ HEADERS += \
ui/patterneditorview.h \
data/project.h \
ui/channelheaderview.h \
uisocket.h
uisocket.h \
ui/patternsequencermodel.h \
ui/patternlistmodel.h \
util/strings.h \
config/colorscheme.h
FORMS += \
mainwindow.ui