UI stuff, undo/redo for pattern and channel editing
parent
4105e38404
commit
c297ac40ba
24
notes
24
notes
|
@ -49,7 +49,21 @@ TODO {
|
|||
- save empty space as a number of skipped rows (make sure to have something keeping a row count!)
|
||||
|
||||
undo {
|
||||
...
|
||||
- single-row pattern edits
|
||||
? multi-select edits
|
||||
|
||||
- channel rename
|
||||
- channel move
|
||||
- channel add
|
||||
- channel delete
|
||||
|
||||
pattern rename
|
||||
pattern move
|
||||
pattern add
|
||||
pattern delete
|
||||
(just have sequencer state stuff up there)
|
||||
|
||||
generic sequencer edit
|
||||
}
|
||||
|
||||
# fix how qt5.12 broke header text (removed elide for now)
|
||||
|
@ -67,15 +81,11 @@ TODO {
|
|||
pattern editor cells can have (dynamic) tool tips; set this up with port names, etc.
|
||||
? de-hardcode the "» " (probably just make it a static const variable somewhere?)
|
||||
make everything relevant check if editing is locked
|
||||
|
||||
make the save routine displace the old file and write a new one
|
||||
}
|
||||
}
|
||||
|
||||
saving and loading {
|
||||
msgpack object
|
||||
|
||||
probably have a field of raw data for (some) node saves, and keep it resident in case of plugin crashes
|
||||
}
|
||||
|
||||
resampler object {
|
||||
one used internally for each note
|
||||
reference to sample
|
||||
|
|
21
save format
21
save format
|
@ -1,4 +1,11 @@
|
|||
msgpack
|
||||
Xybrid's various save formats are CBOR objects; CBOR was chosen over msgpack due to Qt5.12 having built-in support
|
||||
|
||||
probably have a field of raw data for (some) node saves, and keep it resident in case of plugin crashes
|
||||
|
||||
extensions {
|
||||
.xyp - project
|
||||
.xyg - graph
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -7,21 +14,23 @@ msgpack
|
|||
|
||||
project: [
|
||||
"xybrid:project"
|
||||
(uint32 version)
|
||||
(int version)
|
||||
{
|
||||
"meta": { "artist": ... "title": ... "comment": ... etc. }
|
||||
"patterns": [ array of pattern structs ]
|
||||
"sequence": [ array of pattern numbers, uint32, separator is MAX_VALUE ]
|
||||
"sequence": [ array of pattern numbers, int, separator is anything negative ]
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
[ int port, int note, n*(int c, int amt) ] // for each row
|
||||
// any number of empty rows is expressed as an integer number of rows skipped
|
||||
// this number is only written if there are any non-empty rows after
|
||||
]
|
||||
} ]
|
||||
} ] // if no channels have their last row filled, an integer representing the
|
||||
// total number of rows is written as the last entry of the channel array
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ Row& Row::operator=(const Row& o) noexcept {
|
|||
note = o.note;
|
||||
// copy-constructor the underlying vector
|
||||
if (o.params) params.reset(new std::vector<std::array<unsigned char, 2>>(*o.params));
|
||||
else params.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
#include "patterncommands.h"
|
||||
|
||||
#include "uisocket.h"
|
||||
#include "data/project.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUndoStack>
|
||||
|
||||
using Xybrid::Data::Pattern;
|
||||
using namespace Xybrid::Editing;
|
||||
|
||||
bool PatternCommand::commit() {
|
||||
if (!pattern->valid()) return false;
|
||||
if (!pattern->project->socket || !pattern->project->socket->undoStack) return false;
|
||||
pattern->project->socket->undoStack->push(this);
|
||||
return true;
|
||||
}
|
||||
bool PatternCommand::cancel() {
|
||||
delete this;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
PatternDeltaCommand::PatternDeltaCommand(const std::shared_ptr<Pattern>& pattern, int c, int r) {
|
||||
this->pattern = pattern;
|
||||
ch = c;
|
||||
rw = r;
|
||||
oldRow = pattern->rowAt(c, r);
|
||||
row = oldRow;
|
||||
setText("edit row");
|
||||
}
|
||||
|
||||
bool PatternDeltaCommand::mergeWith(const QUndoCommand* o_) {
|
||||
if (o_->id() != id()) return false;
|
||||
auto* o = static_cast<const PatternDeltaCommand*>(o_);
|
||||
if (o->pattern != pattern) return false;
|
||||
if (o->ch != ch || o->rw != rw) return false;
|
||||
row = static_cast<const PatternDeltaCommand*>(o)->row;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PatternDeltaCommand::redo() {
|
||||
pattern->rowAt(ch, rw) = row;
|
||||
emit pattern->project->socket->rowUpdated(pattern.get(), ch, rw);
|
||||
}
|
||||
|
||||
void PatternDeltaCommand::undo() {
|
||||
pattern->rowAt(ch, rw) = oldRow;
|
||||
emit pattern->project->socket->rowUpdated(pattern.get(), ch, rw);
|
||||
}
|
||||
|
||||
PatternChannelMoveCommand::PatternChannelMoveCommand(const std::shared_ptr<Xybrid::Data::Pattern> &pattern, int from, int to) {
|
||||
this->pattern = pattern;
|
||||
this->from = from;
|
||||
this->to = to;
|
||||
setText("move channel");
|
||||
}
|
||||
|
||||
bool PatternChannelMoveCommand::mergeWith(const QUndoCommand* o_) {
|
||||
if (o_->id() != id()) return false;
|
||||
auto* o = static_cast<const PatternChannelMoveCommand*>(o_);
|
||||
if (o->pattern != pattern) return false;
|
||||
if (o->from != to) return false;
|
||||
to = o->to;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PatternChannelMoveCommand::redo() {
|
||||
Pattern::Channel ch = std::move(*(pattern->channels.begin()+from));
|
||||
pattern->channels.erase(pattern->channels.begin()+from);
|
||||
pattern->channels.insert(pattern->channels.begin()+to, std::move(ch));
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternChannelMoveCommand::undo() {
|
||||
Pattern::Channel ch = std::move(*(pattern->channels.begin()+to));
|
||||
pattern->channels.erase(pattern->channels.begin()+to);
|
||||
pattern->channels.insert(pattern->channels.begin()+from, std::move(ch));
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
PatternChannelRenameCommand::PatternChannelRenameCommand(const std::shared_ptr<Xybrid::Data::Pattern>& pattern, int channel, const std::string &to) {
|
||||
this->pattern = pattern;
|
||||
idx = channel;
|
||||
from = pattern->channel(idx).name;
|
||||
this->to = to;
|
||||
setText("rename channel");
|
||||
}
|
||||
|
||||
bool PatternChannelRenameCommand::mergeWith(const QUndoCommand* o_) {
|
||||
if (o_->id() != id()) return false;
|
||||
auto* o = static_cast<const PatternChannelRenameCommand*>(o_);
|
||||
if (o->pattern != pattern) return false;
|
||||
if (o->idx != idx) return false;
|
||||
to = o->to;
|
||||
return true;
|
||||
}
|
||||
|
||||
void PatternChannelRenameCommand::redo() {
|
||||
pattern->channel(idx).name = to;
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternChannelRenameCommand::undo() {
|
||||
pattern->channel(idx).name = from;
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
PatternChannelAddCommand::PatternChannelAddCommand(const std::shared_ptr<Xybrid::Data::Pattern>& pattern, int channel) {
|
||||
this->pattern = pattern;
|
||||
idx = channel;
|
||||
setText("add channel");
|
||||
}
|
||||
|
||||
void PatternChannelAddCommand::redo() {
|
||||
pattern->addChannel(idx);
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternChannelAddCommand::undo() {
|
||||
pattern->deleteChannel(idx);
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
PatternChannelDeleteCommand::PatternChannelDeleteCommand(const std::shared_ptr<Xybrid::Data::Pattern>& pattern, int channel) {
|
||||
this->pattern = pattern;
|
||||
idx = channel;
|
||||
setText("delete channel");
|
||||
}
|
||||
|
||||
void PatternChannelDeleteCommand::redo() {
|
||||
ch = std::move(*(pattern->channels.begin()+idx));
|
||||
pattern->channels.erase(pattern->channels.begin()+idx);
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
||||
|
||||
void PatternChannelDeleteCommand::undo() {
|
||||
pattern->channels.insert(pattern->channels.begin()+idx, std::move(ch));
|
||||
emit pattern->project->socket->patternUpdated(pattern.get());
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include "data/pattern.h"
|
||||
|
||||
#include <QUndoCommand>
|
||||
|
||||
namespace Xybrid::Editing {
|
||||
class PatternCommand : public QUndoCommand {
|
||||
//
|
||||
protected:
|
||||
std::shared_ptr<Data::Pattern> pattern;
|
||||
|
||||
public:
|
||||
bool commit();
|
||||
bool cancel();
|
||||
};
|
||||
|
||||
class PatternDeltaCommand : public PatternCommand {
|
||||
int ch, rw;
|
||||
Data::Pattern::Row oldRow;
|
||||
public:
|
||||
Data::Pattern::Row row;
|
||||
|
||||
PatternDeltaCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel, int row);
|
||||
~PatternDeltaCommand() override = default;
|
||||
|
||||
int id() const override {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
bool mergeWith(const QUndoCommand*) override;
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternChannelMoveCommand : public PatternCommand {
|
||||
int from, to;
|
||||
|
||||
public:
|
||||
PatternChannelMoveCommand(const std::shared_ptr<Data::Pattern>& pattern, int from, int to);
|
||||
~PatternChannelMoveCommand() override = default;
|
||||
|
||||
int id() const override {
|
||||
return 2100;
|
||||
}
|
||||
|
||||
bool mergeWith(const QUndoCommand*) override;
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternChannelRenameCommand : public PatternCommand {
|
||||
int idx;
|
||||
std::string from, to;
|
||||
|
||||
public:
|
||||
PatternChannelRenameCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel, const std::string& to);
|
||||
~PatternChannelRenameCommand() override = default;
|
||||
|
||||
int id() const override {
|
||||
return 2101;
|
||||
}
|
||||
|
||||
bool mergeWith(const QUndoCommand*) override;
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternChannelAddCommand : public PatternCommand {
|
||||
int idx;
|
||||
|
||||
public:
|
||||
PatternChannelAddCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel);
|
||||
~PatternChannelAddCommand() override = default;
|
||||
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
|
||||
class PatternChannelDeleteCommand : public PatternCommand {
|
||||
int idx;
|
||||
Data::Pattern::Channel ch;
|
||||
|
||||
public:
|
||||
PatternChannelDeleteCommand(const std::shared_ptr<Data::Pattern>& pattern, int channel);
|
||||
~PatternChannelDeleteCommand() override = default;
|
||||
|
||||
void redo() override;
|
||||
void undo() override;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
namespace Xybrid::Editing {
|
||||
class PatternLens {
|
||||
public:
|
||||
//
|
||||
};
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
#include "fileops.h"
|
||||
|
||||
#include "uisocket.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <QFile>
|
||||
|
@ -8,6 +10,8 @@
|
|||
#include <QCborStreamReader>
|
||||
#include <QCborStreamWriter>
|
||||
|
||||
#include <QUndoStack>
|
||||
|
||||
using Xybrid::Data::Project;
|
||||
using Xybrid::Data::Pattern;
|
||||
|
||||
|
@ -99,6 +103,8 @@ bool Xybrid::FileOps::saveProject(std::shared_ptr<Project> project, QString file
|
|||
root.toCborValue().toCbor(w);
|
||||
file.close();
|
||||
|
||||
if (project->socket && project->socket->undoStack) project->socket->undoStack->setClean();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ using Xybrid::MainWindow;
|
|||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QUndoStack>
|
||||
|
||||
#include "util/strings.h"
|
||||
#include "fileops.h"
|
||||
|
@ -26,7 +27,7 @@ using Xybrid::UI::PatternEditorModel;
|
|||
using Xybrid::UI::PatternEditorItemDelegate;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const auto projectFilter = u8"Xybrid project (*.xyp)\nAll files (*)";
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) :
|
||||
|
@ -34,8 +35,21 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
ui(new Ui::MainWindow) {
|
||||
ui->setupUi(this);
|
||||
|
||||
auto t = ui->tabWidget;
|
||||
undoStack = new QUndoStack(this);
|
||||
//undoStack->setUndoLimit(256);
|
||||
connect(undoStack, &QUndoStack::cleanChanged, [this](bool) {
|
||||
updateTitle();
|
||||
});
|
||||
|
||||
auto* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
|
||||
undoAction->setShortcuts(QKeySequence::Undo);
|
||||
ui->menuEdit->addAction(undoAction);
|
||||
|
||||
auto* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
|
||||
redoAction->setShortcuts(QKeySequence::Redo);
|
||||
ui->menuEdit->addAction(redoAction);
|
||||
|
||||
auto t = ui->tabWidget;
|
||||
t->setCornerWidget(ui->menuBar);
|
||||
t->setCornerWidget(ui->label, Qt::TopLeftCorner);
|
||||
auto mb = ui->menuBar;
|
||||
|
@ -190,7 +204,19 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
|
||||
// Set up signaling from project to UI
|
||||
socket.reset(new UISocket());
|
||||
socket->undoStack = undoStack;
|
||||
connect(socket.get(), &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
|
||||
connect(socket.get(), &UISocket::patternUpdated, [this](Pattern* p) {
|
||||
if (editingPattern.get() != p) return;
|
||||
ui->patternEditor->refresh();
|
||||
});
|
||||
connect(socket.get(), &UISocket::rowUpdated, [this](Pattern* p, int ch, int r) {
|
||||
if (editingPattern.get() != p) return;
|
||||
const auto cpc = PatternEditorModel::colsPerChannel;
|
||||
auto ind = ui->patternEditor->model()->index(r, ch * cpc);
|
||||
emit ui->patternEditor->model()->dataChanged(ind, ind.siblingAtColumn((ch+1)*cpc-1));
|
||||
static_cast<PatternEditorModel*>(ui->patternEditor->model())->updateColumnDisplay();
|
||||
});
|
||||
|
||||
// and start with a new project
|
||||
menuFileNew();
|
||||
|
@ -229,7 +255,7 @@ void MainWindow::menuFileNew() {
|
|||
}
|
||||
|
||||
void MainWindow::menuFileOpen() {
|
||||
auto fileName = QFileDialog::getOpenFileName(this, "Open project...");
|
||||
auto fileName = QFileDialog::getOpenFileName(this, "Open project...", QString(), projectFilter);
|
||||
if (fileName.isEmpty()) return; // canceled
|
||||
auto np = FileOps::loadProject(fileName);
|
||||
if (!np) {
|
||||
|
@ -248,12 +274,14 @@ void MainWindow::menuFileSave() {
|
|||
}
|
||||
|
||||
void MainWindow::menuFileSaveAs() {
|
||||
auto fileName = QFileDialog::getSaveFileName(this, "Save project as...");
|
||||
auto fileName = QFileDialog::getSaveFileName(this, "Save project as...", QString(), projectFilter);
|
||||
if (fileName.isEmpty()) return; // canceled
|
||||
FileOps::saveProject(project, fileName);
|
||||
}
|
||||
|
||||
void MainWindow::onNewProjectLoaded() {
|
||||
undoStack->clear();
|
||||
|
||||
project->socket = socket.get();
|
||||
Pattern* pt = project->patterns.front().get();
|
||||
for (auto* p : project->sequence) { // find first non-spacer in sequence, else fall back to first pattern numerically
|
||||
|
@ -263,6 +291,8 @@ void MainWindow::onNewProjectLoaded() {
|
|||
}
|
||||
updatePatternLists();
|
||||
selectPatternForEditing(pt);
|
||||
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
void MainWindow::updatePatternLists() {
|
||||
|
@ -272,6 +302,15 @@ void MainWindow::updatePatternLists() {
|
|||
selectPatternForEditing(project->patterns[std::min(editingPattern->index, project->patterns.size() - 1)].get());
|
||||
}
|
||||
|
||||
void MainWindow::updateTitle() {
|
||||
QString title = u8"Xybrid - %1%2";
|
||||
if (project->fileName.isEmpty()) title = title.arg("(new project)");
|
||||
else title = title.arg(QFileInfo(project->fileName).baseName());
|
||||
if (undoStack->isClean()) title = title.arg("");
|
||||
else title = title.arg("*");
|
||||
this->setWindowTitle(title);
|
||||
}
|
||||
|
||||
bool MainWindow::selectPatternForEditing(Pattern* pattern) {
|
||||
if (!pattern || pattern == editingPattern.get()) return false; // no u
|
||||
if (pattern->project != project.get()) return false; // wrong project
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include "data/project.h"
|
||||
#include "ui/patterneditormodel.h"
|
||||
|
||||
class QUndoStack;
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
@ -24,10 +26,14 @@ namespace Xybrid {
|
|||
std::shared_ptr<Data::Project> project;
|
||||
std::shared_ptr<Data::Pattern> editingPattern; // temporary pattern for testing the editor
|
||||
|
||||
QUndoStack* undoStack;
|
||||
|
||||
void onNewProjectLoaded();
|
||||
void updatePatternLists();
|
||||
bool selectPatternForEditing(Data::Pattern*);
|
||||
|
||||
void updateTitle();
|
||||
|
||||
public:
|
||||
Data::Project* getProject() const {
|
||||
return project.get();
|
||||
|
|
|
@ -9,6 +9,9 @@ using Xybrid::UI::PatternEditorView;
|
|||
#include "ui/patterneditormodel.h"
|
||||
using Xybrid::UI::PatternEditorModel;
|
||||
|
||||
#include "editing/patterncommands.h"
|
||||
using namespace Xybrid::Editing;
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -90,18 +93,20 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
if (index.data().isNull()) return false; // no channels?
|
||||
auto type = event->type();
|
||||
if (type == QEvent::KeyPress) {
|
||||
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
|
||||
|
||||
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();
|
||||
int cc = index.column() % PatternEditorModel::colsPerChannel;
|
||||
int ch = (index.column() - cc) / PatternEditorModel::colsPerChannel;
|
||||
auto& row = p->rowAt(ch, index.row());
|
||||
auto* dc = new PatternDeltaCommand(p, ch, index.row());
|
||||
auto& row = dc->row;//p->rowAt(ch, index.row());
|
||||
|
||||
auto* sm = static_cast<PatternEditorView*>(parent())->selectionModel();
|
||||
auto sel = sm->selectedIndexes();
|
||||
|
||||
if (static_cast<QKeyEvent*>(event)->isAutoRepeat()) return false; // reject autorepeat
|
||||
if (mod & Qt::Modifier::CTRL) {
|
||||
|
||||
} else if (mod & Qt::Modifier::ALT) {
|
||||
|
@ -138,6 +143,7 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
//editorEvent(event, model, option, *i);
|
||||
}
|
||||
for (auto s : sel) sm->select(s, QItemSelectionModel::Select);*/
|
||||
delete dc;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -148,26 +154,26 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
if (cc == 0) { // port column
|
||||
if (k == Qt::Key_Delete) {
|
||||
row.port = -1;
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
if (k == numberKeys[i]) {
|
||||
insertDigit(row.port, i);
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
}
|
||||
} else if (cc == 1) { // note column
|
||||
if (k == Qt::Key_Delete) {
|
||||
row.note = -1;
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
if (k == Qt::Key_Equal) { // note off
|
||||
row.note = -2;
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
if (k == Qt::Key_Plus) { // shift for hard cut; for some reason this is a separate keycode
|
||||
row.note = -3;
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
for (size_t i = 0; i < (sizeof(pianoKeys) / sizeof(pianoKeys[0])); i++) {
|
||||
if (k == pianoKeys[i]) { // piano input
|
||||
|
@ -178,17 +184,15 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
auto& r = p->rowAt(ch, i);
|
||||
if (r.port >= 0 && r.note != -1) {
|
||||
row.port = r.port;
|
||||
auto ind = index.siblingAtColumn(index.column() - 1);
|
||||
emit model->dataChanged(ind, ind, {Qt::DisplayRole});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return dc->commit();
|
||||
} else if (i < 10 && k == numberKeys[i]) { // set octave
|
||||
if (row.note >= 0) row.note = static_cast<int16_t>((row.note % 12) + 12*i);
|
||||
static_cast<PatternEditorModel*>(model)->updateColumnDisplay();
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
}
|
||||
} else { // param column
|
||||
|
@ -196,39 +200,33 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
if (k == Qt::Key_Insert) { // insert from within any place in the param columns
|
||||
if (row.numParams() >= PatternEditorModel::paramSoftCap) return false; // no overruns
|
||||
row.insertParam(par);
|
||||
// update whole row (phew!)
|
||||
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
|
||||
m->updateColumnDisplay(); // update column autohide
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
size_t cpar = row.numParams() - 1;
|
||||
if (cpar > par) cpar = par;
|
||||
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(cpar) * 2 + 2));
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
if (par < row.numParams()) {
|
||||
if (k == Qt::Key_Delete) { // remove selected parameter
|
||||
row.removeParam(par);
|
||||
// update whole row (phew!)
|
||||
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
|
||||
m->updateColumnDisplay(); // update column autohide
|
||||
if (par >= row.numParams()) { // snap to arrow if beyond
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 2));
|
||||
}
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return false;
|
||||
if (k == Qt::Key_Delete || k == Qt::Key_Insert || k == Qt::Key_Backspace) return dc->cancel();
|
||||
if (cc % 2 == 0) { // char column; set to key pressed and move forward
|
||||
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
|
||||
row.param(par)[0] = static_cast<unsigned char>(chr);
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
view->setCurrentIndex(index.siblingAtColumn(index.column()+1));
|
||||
return true;
|
||||
return dc->commit();
|
||||
} else {
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
if (k == numberKeys[i]) {
|
||||
insertDigit(row.param(par)[1], i);
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,15 +235,14 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m
|
|||
char chr = static_cast<QKeyEvent*>(event)->text().toUtf8()[0];
|
||||
row.addParam(chr);
|
||||
// update whole row (phew!)
|
||||
emit model->dataChanged(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel), index.siblingAtColumn((ch+1) * PatternEditorModel::colsPerChannel-1), {Qt::DisplayRole});
|
||||
m->updateColumnDisplay(); // update column autohide
|
||||
auto view = static_cast<PatternEditorView*>(parent());
|
||||
view->setCurrentIndex(index.siblingAtColumn(ch * PatternEditorModel::colsPerChannel + static_cast<int>(row.numParams()) * 2 + 1));
|
||||
return true;
|
||||
return dc->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// kill command if unused
|
||||
delete dc;
|
||||
}
|
||||
return false;
|
||||
}//*/
|
||||
|
|
|
@ -14,6 +14,9 @@ using Xybrid::UI::ChannelHeaderView;
|
|||
using Xybrid::Data::Project;
|
||||
using Xybrid::Data::Pattern;
|
||||
|
||||
#include "editing/patterncommands.h"
|
||||
using namespace Xybrid::Editing;
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QDebug>
|
||||
|
||||
|
@ -131,6 +134,10 @@ void PatternEditorView::updateHeader(bool full) {
|
|||
hdr->setGeometry(bh->x(), bh->y(), bh->width(), bh->height());
|
||||
hdr->setOffset(bh->offset());
|
||||
}
|
||||
|
||||
void PatternEditorView::refresh() {
|
||||
mdl->refresh();
|
||||
}
|
||||
void PatternEditorView::updateHeaderOffset(int) {
|
||||
updateHeader(false);
|
||||
}
|
||||
|
@ -140,21 +147,7 @@ void PatternEditorView::headerMoved(int logicalIndex, int oldVisualIndex, int ne
|
|||
hdr->moveSection(newVisualIndex, logicalIndex); // maintain straight-through order
|
||||
if (logicalIndex >= hdr->count() - 1) return; // no dragging the endcap :|
|
||||
|
||||
int min = std::min(oldVisualIndex, newVisualIndex);
|
||||
int max = std::max(oldVisualIndex, newVisualIndex);
|
||||
int nf = 0;
|
||||
if (newVisualIndex > oldVisualIndex) nf = min + 1;
|
||||
else nf = max;
|
||||
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()),
|
||||
mdl->index(mdl->rowCount() - 1, mdl->columnCount() - 1, QModelIndex())
|
||||
);
|
||||
|
||||
mdl->updateColumnDisplay(); // update geometries and hide flags
|
||||
// and make sure header follows
|
||||
emit hdr->model()->headerDataChanged(Qt::Horizontal, 0, static_cast<int>(chn.size()) - 1);
|
||||
(new PatternChannelMoveCommand(mdl->getPattern(), oldVisualIndex, newVisualIndex))->commit();
|
||||
}
|
||||
|
||||
void PatternEditorView::headerDoubleClicked(int section) {
|
||||
|
@ -166,15 +159,13 @@ void PatternEditorView::headerContextMenu(QPoint pt) {
|
|||
std::shared_ptr<Pattern> p = mdl->getPattern();
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->addAction("Add Channel", [this, idx, p]() {
|
||||
p->addChannel(idx);
|
||||
mdl->refresh();
|
||||
menu->addAction("Add Channel", [/*this,*/ idx, p]() {
|
||||
(new PatternChannelAddCommand(p, idx))->commit();
|
||||
});
|
||||
if (idx < hdr->count() - 1) {
|
||||
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();
|
||||
(new PatternChannelDeleteCommand(p, idx))->commit();
|
||||
});
|
||||
menu->addAction("Rename Channel", [this, idx, p]() {
|
||||
if (p != mdl->getPattern()) return; // swapped already
|
||||
|
@ -194,6 +185,5 @@ void PatternEditorView::startRenameChannel(int 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
|
||||
c->name = n.toStdString(); // and set name
|
||||
mdl->updateColumnDisplay(); // update sizes and such
|
||||
(new PatternChannelRenameCommand(p, channel, n.toStdString()))->commit();
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ namespace Xybrid::UI {
|
|||
|
||||
void updateHeader(bool full = false);
|
||||
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void updateHeaderOffset(int);
|
||||
void headerMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
|
||||
|
|
|
@ -2,10 +2,22 @@
|
|||
|
||||
#include <QObject>
|
||||
|
||||
class QUndoStack;
|
||||
|
||||
namespace Xybrid::Data {
|
||||
class Project;
|
||||
class Pattern;
|
||||
}
|
||||
|
||||
namespace Xybrid {
|
||||
class UISocket : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
QUndoStack* undoStack;
|
||||
|
||||
signals:
|
||||
void updatePatternLists();
|
||||
void patternUpdated(Data::Pattern* pattern);
|
||||
void rowUpdated(Data::Pattern* pattern, int channel, int row);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ SOURCES += \
|
|||
ui/patternsequencermodel.cpp \
|
||||
ui/patternlistmodel.cpp \
|
||||
config/colorscheme.cpp \
|
||||
fileops.cpp
|
||||
fileops.cpp \
|
||||
editing/patterncommands.cpp
|
||||
|
||||
HEADERS += \
|
||||
mainwindow.h \
|
||||
|
@ -53,7 +54,9 @@ HEADERS += \
|
|||
ui/patternlistmodel.h \
|
||||
util/strings.h \
|
||||
config/colorscheme.h \
|
||||
fileops.h
|
||||
fileops.h \
|
||||
editing/patternlens.h \
|
||||
editing/patterncommands.h
|
||||
|
||||
FORMS += \
|
||||
mainwindow.ui
|
||||
|
|
Loading…
Reference in New Issue