pattern properties dialog!
parent
cc5ef1534d
commit
9bb2a38a90
4
notes
4
notes
|
@ -48,7 +48,7 @@ TODO {
|
|||
- transpose selection (alt+up/down for semitone/0x01, left+right for octave/0x10)
|
||||
- pattern cut+copy+paste
|
||||
- ^ channel-column splitting (partial channel copy/paste)
|
||||
add metadata and pattern properties (artist, song title, project bpm; pattern name, length etc.)
|
||||
- pattern properties dialog (name, length, time signature)
|
||||
|
||||
probably move the "process all nodes" part of tick processing into its own function?
|
||||
|
||||
|
@ -65,6 +65,8 @@ TODO {
|
|||
|
||||
|
||||
misc features needed before proper release {
|
||||
song metadata (title, artist, comment, default bpm)
|
||||
|
||||
at *least* js plugin support, with lua+lv2 highly preferable
|
||||
SAMPLES and SAMPLING
|
||||
|
||||
|
|
|
@ -18,6 +18,9 @@ namespace Xybrid::Data {
|
|||
TimeSignature() = default;
|
||||
TimeSignature(int b, int r, int t) : beatsPerMeasure(b), rowsPerBeat(r), ticksPerRow(t) {}
|
||||
constexpr int rowsPerMeasure() const { return beatsPerMeasure * rowsPerBeat; }
|
||||
|
||||
constexpr bool operator==(const TimeSignature& o) const { return beatsPerMeasure == o.beatsPerMeasure && rowsPerBeat == o.rowsPerBeat && ticksPerRow == o.ticksPerRow; }
|
||||
constexpr bool operator!=(const TimeSignature& o) const { return !(*this == o); }
|
||||
};
|
||||
class Pattern {
|
||||
public:
|
||||
|
|
|
@ -74,6 +74,56 @@ void PatternRenameCommand::undo() {
|
|||
emit pattern->project->socket->updatePatternLists();
|
||||
}
|
||||
|
||||
PatternTimeSignatureCommand::PatternTimeSignatureCommand(const std::shared_ptr<Xybrid::Data::Pattern>& pattern, const Xybrid::Data::TimeSignature& to) {
|
||||
this->pattern = pattern;
|
||||
from = pattern->time;
|
||||
this->to = to;
|
||||
setText("edit pattern time signature");
|
||||
}
|
||||
|
||||
bool PatternTimeSignatureCommand::mergeWith(const QUndoCommand* o_) {
|
||||
if (o_->id() != id()) return false;
|
||||
auto* o = static_cast<const PatternTimeSignatureCommand*>(o_);
|
||||
if (o->pattern != pattern) return false;
|
||||
to = o->to;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PatternTimeSignatureCommand::redo() {
|
||||
pattern->time = to;
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternTimeSignatureCommand::undo() {
|
||||
pattern->time = from;
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
PatternLengthCommand::PatternLengthCommand(const std::shared_ptr<Xybrid::Data::Pattern>& pattern, int to) {
|
||||
this->pattern = pattern;
|
||||
from = pattern->rows;
|
||||
this->to = to;
|
||||
setText("resize pattern");
|
||||
}
|
||||
|
||||
bool PatternLengthCommand::mergeWith(const QUndoCommand* o_) {
|
||||
if (o_->id() != id()) return false;
|
||||
auto* o = static_cast<const PatternLengthCommand*>(o_);
|
||||
if (o->pattern != pattern) return false;
|
||||
to = o->to;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PatternLengthCommand::redo() {
|
||||
pattern->setLength(to);
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternLengthCommand::undo() {
|
||||
pattern->setLength(from);
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
PatternChannelMoveCommand::PatternChannelMoveCommand(const std::shared_ptr<Pattern> &pattern, int from, int to) {
|
||||
this->pattern = pattern;
|
||||
this->from = from;
|
||||
|
|
|
@ -48,6 +48,32 @@ namespace Xybrid::Editing {
|
|||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternTimeSignatureCommand : public PatternCommand {
|
||||
Data::TimeSignature from, to;
|
||||
|
||||
public:
|
||||
PatternTimeSignatureCommand(const std::shared_ptr<Data::Pattern>& pattern, const Data::TimeSignature& to);
|
||||
~PatternTimeSignatureCommand() override = default;
|
||||
|
||||
int id() const override { return 2071; }
|
||||
bool mergeWith(const QUndoCommand*) override;
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternLengthCommand : public PatternCommand {
|
||||
int from, to;
|
||||
|
||||
public:
|
||||
PatternLengthCommand(const std::shared_ptr<Data::Pattern>& pattern, int to);
|
||||
~PatternLengthCommand() override = default;
|
||||
|
||||
int id() const override { return 2072; }
|
||||
bool mergeWith(const QUndoCommand*) override;
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternChannelMoveCommand : public PatternCommand {
|
||||
int from, to;
|
||||
|
||||
|
|
|
@ -63,8 +63,6 @@ void Transpose::process() {
|
|||
}
|
||||
mi += 5 + out->data[mi+4]*2;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void Transpose::saveData(QCborMap& m) {
|
||||
|
|
|
@ -9,6 +9,8 @@ using Xybrid::MainWindow;
|
|||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QSpinBox>
|
||||
#include <QWindow>
|
||||
#include <QUndoStack>
|
||||
#include <QTimer>
|
||||
|
@ -32,7 +34,9 @@ using Xybrid::MainWindow;
|
|||
|
||||
#include "ui/patchboard/patchboardscene.h"
|
||||
|
||||
#include "editing/compositecommand.h"
|
||||
#include "editing/projectcommands.h"
|
||||
#include "editing/patterncommands.h"
|
||||
|
||||
#include "config/pluginregistry.h"
|
||||
#include "audio/audioengine.h"
|
||||
|
@ -83,6 +87,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
ui->menuEdit->addAction(redoAction);
|
||||
|
||||
auto* t = ui->tabWidget;
|
||||
t->setCurrentWidget(ui->pattern); // set default regardless of what was edited last in the designer :|
|
||||
t->setCornerWidget(ui->menuBar);
|
||||
t->setCornerWidget(ui->logo, Qt::TopLeftCorner);
|
||||
//ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
|
||||
|
@ -107,7 +112,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
this->selectPatternForEditing(project->patterns[idx].get());
|
||||
});
|
||||
// on click
|
||||
connect(ui->patternList, &QListView::clicked, this, [this]() { // deselect on sequencer when list clicked
|
||||
connect(ui->patternList, &QListView::clicked, this, [this] { // deselect on sequencer when list clicked
|
||||
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, -1));
|
||||
});
|
||||
|
||||
|
@ -118,15 +123,17 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
if (idx < project->patterns.size()) p = project->patterns[idx];
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->addAction("New Pattern", this, [this, idx]() {
|
||||
menu->addAction("New Pattern", this, [this, idx] {
|
||||
(new ProjectPatternAddCommand(project, static_cast<int>(idx)))->commit();
|
||||
});
|
||||
if (p) {
|
||||
menu->addAction("Duplicate Pattern", this, [this, p, idx]() {
|
||||
menu->addAction("Duplicate Pattern", this, [this, p, idx] {
|
||||
(new ProjectPatternAddCommand(project, static_cast<int>(idx) + 1, -1, p))->commit();
|
||||
});
|
||||
menu->addSeparator();
|
||||
menu->addAction("Delete Pattern", this, [this, p]() {
|
||||
menu->addAction("Properties...", this, [this, p] { openPatternProperties(p); });
|
||||
menu->addSeparator();
|
||||
menu->addAction("Delete Pattern", this, [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;
|
||||
(new ProjectPatternDeleteCommand(project, p))->commit();
|
||||
});
|
||||
|
@ -158,7 +165,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
size_t idx = static_cast<size_t>(ui->patternSequencer->indexAt(pt).column());
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->addAction("Insert Pattern", this, [this, idx]() {
|
||||
menu->addAction("Insert Pattern", this, [this, idx] {
|
||||
if (!editingPattern->validFor(project)) return; // nope
|
||||
int si = static_cast<int>(std::min(idx, project->sequence.size()));
|
||||
auto* c = new ProjectSequencerDeltaCommand(project);
|
||||
|
@ -166,29 +173,31 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
c->seqSel = si+1;
|
||||
c->commit();
|
||||
});
|
||||
menu->addAction("Insert Separator", this, [this, idx]() {
|
||||
menu->addAction("Insert Separator", this, [this, idx] {
|
||||
int si = static_cast<int>(std::min(idx, project->sequence.size()));
|
||||
auto* c = new ProjectSequencerDeltaCommand(project);
|
||||
c->seq.insert(c->seq.begin() + si, nullptr);
|
||||
c->seqSel = si+1;
|
||||
c->commit();
|
||||
});
|
||||
if (idx < project->sequence.size()) menu->addAction("Remove", this, [this, idx]() {
|
||||
if (idx < project->sequence.size()) menu->addAction("Remove", this, [this, idx] {
|
||||
auto* c = new ProjectSequencerDeltaCommand(project);
|
||||
c->seq.erase(c->seq.begin() + static_cast<ptrdiff_t>(idx));
|
||||
c->seqSel = static_cast<int>(idx)-1;
|
||||
c->commit();
|
||||
});
|
||||
menu->addSeparator();
|
||||
menu->addAction("Create New Pattern", this, [this, idx]() {
|
||||
menu->addAction("Create New Pattern", this, [this, idx] {
|
||||
int si = static_cast<int>(std::min(idx, project->sequence.size()));
|
||||
(new ProjectPatternAddCommand(project, -1, si))->commit();
|
||||
});
|
||||
if (idx < project->sequence.size() && project->sequence[idx]) {
|
||||
menu->addAction("Duplicate Pattern", this, [this, idx, p = project->patterns[project->sequence[idx]->index]]() {
|
||||
menu->addAction("Duplicate Pattern", this, [this, idx, p = project->patterns[project->sequence[idx]->index]] {
|
||||
int si = static_cast<int>(std::min(idx + 1, project->sequence.size()));
|
||||
(new ProjectPatternAddCommand(project, static_cast<int>(p->index) + 1, si, p))->commit();
|
||||
});
|
||||
menu->addSeparator();
|
||||
menu->addAction("Properties...", this, [this, p = project->patterns[project->sequence[idx]->index]] { openPatternProperties(p); });
|
||||
}
|
||||
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
@ -198,7 +207,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
|
||||
{ /* 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, [this]() {
|
||||
connect(new QShortcut(QKeySequence("Ctrl+PgUp"), ui->pattern), &QShortcut::activated, this, [this] {
|
||||
auto i = ui->patternSequencer->currentIndex();
|
||||
if (!i.isValid()) {
|
||||
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(ui->patternSequencer->horizontalHeader()->count() - 1, 0));
|
||||
|
@ -207,7 +216,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
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, [this]() {
|
||||
connect(new QShortcut(QKeySequence("Ctrl+PgDown"), ui->pattern), &QShortcut::activated, this, [this] {
|
||||
auto i = ui->patternSequencer->currentIndex();
|
||||
if (!i.isValid()) {
|
||||
ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, 0));
|
||||
|
@ -218,13 +227,13 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
});
|
||||
|
||||
// TEMP - play/stop
|
||||
connect(new QShortcut(QKeySequence("Ctrl+P"), this), &QShortcut::activated, this, [this]() {
|
||||
connect(new QShortcut(QKeySequence("Ctrl+P"), this), &QShortcut::activated, this, [this] {
|
||||
if (audioEngine->playbackMode() == AudioEngine::Playing) audioEngine->stop();
|
||||
else audioEngine->play(project);
|
||||
});
|
||||
|
||||
/* tmp test
|
||||
connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, this, [this]() {
|
||||
connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, this, [this] {
|
||||
auto inp = QInputDialog::getText(this, "yes", "yes");
|
||||
WId id = inp.toULongLong();
|
||||
auto* w = QWindow::fromWinId(id);
|
||||
|
@ -312,7 +321,7 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
});
|
||||
|
||||
// and from audio engine
|
||||
connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction]() {
|
||||
connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction] {
|
||||
bool locked = project->editingLocked();
|
||||
undoAction->setEnabled(!locked);
|
||||
redoAction->setEnabled(!locked);
|
||||
|
@ -463,3 +472,76 @@ void MainWindow::openGraph(const std::shared_ptr<Data::Graph>& g) {
|
|||
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 0);
|
||||
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 1);
|
||||
}
|
||||
|
||||
void MainWindow::openPatternProperties(const std::shared_ptr<Xybrid::Data::Pattern>& p) {
|
||||
if (!p || p->project != project.get()) return;
|
||||
auto dlg = new QDialog(this);
|
||||
dlg->setWindowTitle("Pattern Properties");
|
||||
dlg->setModal(true);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
//dlg->resize(320, 200);
|
||||
auto dl = new QVBoxLayout();
|
||||
dlg->setLayout(dl);
|
||||
//dlg->layout()->setMargin(3);
|
||||
|
||||
auto eName = new QLineEdit(QString::fromStdString(p->name));
|
||||
dl->addWidget(eName);
|
||||
//dlg->layout()->setAlignment(eName, Qt::AlignTop);
|
||||
|
||||
auto gLength = new QHBoxLayout();
|
||||
dl->addLayout(gLength);
|
||||
gLength->addWidget(new QLabel("Length"));
|
||||
auto gLengthBox = new QSpinBox();
|
||||
gLengthBox->setRange(1, 1024);
|
||||
gLengthBox->setValue(p->rows);
|
||||
gLengthBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
gLength->addWidget(gLengthBox);
|
||||
|
||||
auto gTime = new QHBoxLayout();
|
||||
dl->addLayout(gTime);
|
||||
auto gTimeBoxW = 60;
|
||||
auto gTimeBoxMax = 32;
|
||||
gTime->addWidget(new QLabel("Beats"));
|
||||
auto gTimeBeats = new QSpinBox();
|
||||
gTimeBeats->setRange(1, gTimeBoxMax);
|
||||
gTimeBeats->setValue(p->time.beatsPerMeasure);
|
||||
gTimeBeats->setMaximumWidth(gTimeBoxW);
|
||||
gTime->addWidget(gTimeBeats);
|
||||
gTime->addWidget(new QLabel("Rows"));
|
||||
auto gTimeRows = new QSpinBox();
|
||||
gTimeRows->setRange(1, gTimeBoxMax);
|
||||
gTimeRows->setValue(p->time.rowsPerBeat);
|
||||
gTimeRows->setMaximumWidth(gTimeBoxW);
|
||||
gTime->addWidget(gTimeRows);
|
||||
gTime->addWidget(new QLabel("Ticks"));
|
||||
auto gTimeTicks = new QSpinBox();
|
||||
gTimeTicks->setRange(1, gTimeBoxMax);
|
||||
gTimeTicks->setValue(p->time.ticksPerRow);
|
||||
gTimeTicks->setMaximumWidth(gTimeBoxW);
|
||||
gTime->addWidget(gTimeTicks);
|
||||
|
||||
auto bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
dl->addWidget(bbox);
|
||||
connect(bbox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
|
||||
connect(bbox, &QDialogButtonBox::accepted, dlg, [=] {
|
||||
auto cc = new CompositeCommand();
|
||||
if (auto n = eName->text().toStdString(); n != p->name) cc->compose(new PatternRenameCommand(p, n));
|
||||
if (auto t = Data::TimeSignature(gTimeBeats->value(), gTimeRows->value(), gTimeTicks->value()); t != p->time)
|
||||
cc->compose(new PatternTimeSignatureCommand(p, t));
|
||||
if (auto nr = gLengthBox->value(); nr != p->rows) {
|
||||
if (nr < p->rows) { // preserve contents beyond cutoff
|
||||
for (auto r = nr; r < p->rows; r++) {
|
||||
for (auto c = 0; c < static_cast<int>(p->numChannels()); c++) {
|
||||
cc->compose(new PatternDeltaCommand(p, c, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
cc->compose(new PatternLengthCommand(p, nr));
|
||||
}
|
||||
|
||||
cc->commit("edit pattern properties");
|
||||
dlg->accept();
|
||||
});
|
||||
dlg->show();
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ namespace Xybrid {
|
|||
|
||||
void openGraph(const std::shared_ptr<Data::Graph>&);
|
||||
|
||||
void openPatternProperties(const std::shared_ptr<Data::Pattern>&);
|
||||
|
||||
void updateTitle();
|
||||
|
||||
public:
|
||||
|
|
Loading…
Reference in New Issue