so so much UI stuff (full multi pattern and sequence editing!)
parent
d8af8f463b
commit
701218936b
26
notes
26
notes
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
} ]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#include "colorscheme.h"
|
||||
using Xybrid::Config::ColorScheme;
|
||||
|
||||
ColorScheme ColorScheme::current;
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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*);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -6,6 +6,6 @@ namespace Xybrid {
|
|||
class UISocket : public QObject {
|
||||
Q_OBJECT
|
||||
signals:
|
||||
//
|
||||
void updatePatternLists();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue