patchboard cut+copy+paste (and delete shortcut)
parent
7238916c24
commit
6c6c62e4ab
16
notes
16
notes
|
@ -45,13 +45,13 @@ project data {
|
|||
|
||||
TODO {
|
||||
immediate frontburner {
|
||||
patchboard copy+paste! cbor array of node objects
|
||||
position: copy relative to centroid, paste relative to center of viewport
|
||||
also record connections between those particular nodes!
|
||||
- patchboard copy+paste! cbor array of node objects
|
||||
- position: copy relative to centroid, paste relative to center of viewport
|
||||
- also record connections between those particular nodes!
|
||||
- provide conversions to/from qcborvalue in node.cpp?
|
||||
- while we're here, add file import/export of any given node (.xyn)
|
||||
|
||||
probably exclude i/o ports from copy+paste lest things futz up on the parent
|
||||
- probably exclude i/o ports from copy+paste lest things futz up on the parent
|
||||
}
|
||||
|
||||
-? return the latency/buffer size to 100ms once multithreading is streamlined
|
||||
|
@ -65,7 +65,7 @@ TODO {
|
|||
switch InstrumentCore over to central storage for notes/tweens
|
||||
|
||||
bugs to fix {
|
||||
graph connections sometimes spawn in duplicated :|
|
||||
-? graph connections sometimes spawn in duplicated :|
|
||||
|
||||
on starting playback, sometimes a "thunk" sneaks into the waveform?
|
||||
|
||||
|
@ -112,6 +112,12 @@ TODO {
|
|||
}
|
||||
}
|
||||
|
||||
Xynamo (hybrid? synth) {
|
||||
3? 5? oscillators; several different waveforms available, independent filtering, wavefolding and (hard) wrapping...
|
||||
individually controllable??
|
||||
rail system!
|
||||
}
|
||||
|
||||
- dumb per-cycle atomic memory allocator from fixed pool for port buffer allocations
|
||||
? can also set up a tlsf pool per worker; prefix allocations with single byte identifier indicating which one they came from,
|
||||
? and defer freeing operations via message queues
|
||||
|
|
|
@ -93,6 +93,7 @@ void Graph::loadData(const QCborMap& m) {
|
|||
|
||||
{ /* children */ } {
|
||||
QCborArray c = m.value("children").toArray();
|
||||
children.reserve(static_cast<size_t>(c.size()));
|
||||
for (auto chmv : c) Node::fromCbor(chmv, g); // oh, that's it?
|
||||
}
|
||||
|
||||
|
|
|
@ -12,14 +12,17 @@ using namespace Xybrid::Config;
|
|||
#include "audio/audioengine.h"
|
||||
using namespace Xybrid::Audio;
|
||||
|
||||
#include "util/strings.h"
|
||||
#include "util/ycombinator.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
#include <QMetaEnum>
|
||||
#include <QCborValue>
|
||||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
|
||||
Port::~Port() {
|
||||
// clean up others' connection lists
|
||||
|
@ -77,17 +80,6 @@ std::shared_ptr<Port> Port::makePort(DataType dt) {
|
|||
return std::make_shared<Port>();
|
||||
}
|
||||
|
||||
std::shared_ptr<Node> Node::fromCbor(const QCborMap& m, std::shared_ptr<Graph> parent) {
|
||||
auto ch = PluginRegistry::createInstance(m.value("id").toString().toStdString());
|
||||
ch->parentTo(parent);
|
||||
ch->name = m.value("name").toString().toStdString();
|
||||
ch->x = static_cast<int>(m.value("x").toInteger());
|
||||
ch->y = static_cast<int>(m.value("y").toInteger());
|
||||
ch->loadData(m);
|
||||
return ch;
|
||||
}
|
||||
std::shared_ptr<Node> Node::fromCbor(const QCborValue& m, std::shared_ptr<Graph> parent) { return fromCbor(m.toMap(), parent); }
|
||||
|
||||
QCborMap Node::toCbor() const {
|
||||
QCborMap m;
|
||||
|
||||
|
@ -100,6 +92,123 @@ QCborMap Node::toCbor() const {
|
|||
return m;
|
||||
}
|
||||
|
||||
std::shared_ptr<Node> Node::fromCbor(const QCborMap& m, std::shared_ptr<Graph> parent) {
|
||||
auto ch = PluginRegistry::createInstance(m.value("id").toString().toStdString());
|
||||
ch->parentTo(parent);
|
||||
ch->name = m.value("name").toString().toStdString();
|
||||
ch->x = static_cast<int>(m.value("x").toInteger());
|
||||
ch->y = static_cast<int>(m.value("y").toInteger());
|
||||
ch->loadData(m);
|
||||
return ch;
|
||||
}
|
||||
std::shared_ptr<Node> Node::fromCbor(const QCborValue& m, std::shared_ptr<Graph> parent) { return fromCbor(m.toMap(), parent); }
|
||||
|
||||
QCborMap Node::multiToCbor(std::vector<std::shared_ptr<Node>>& v) {
|
||||
QCborMap m;
|
||||
|
||||
std::unordered_map<Node*, int> indices;
|
||||
{ /* nodes */ } {
|
||||
QCborArray nm;
|
||||
int idx = 0;
|
||||
for (auto n : v) {
|
||||
if (n->isVolatile()) continue; // skip things with volatile locality (i/o ports etc.)
|
||||
indices[n.get()] = idx++;
|
||||
nm << n->toCbor();
|
||||
}
|
||||
m.insert(QString("nodes"), nm);
|
||||
}
|
||||
|
||||
{ /* connections */ } {
|
||||
QCborArray cm;
|
||||
|
||||
for (auto n : v) {
|
||||
if (n->isVolatile()) continue; // already skipped
|
||||
int idx = indices[n.get()];
|
||||
for (auto dt : n->outputs) {
|
||||
for (auto op : dt.second) {
|
||||
auto o = op.second;
|
||||
o->cleanConnections(); // let's just do some groundskeeping here
|
||||
for (auto iw : o->connections) {
|
||||
auto i = iw.lock();
|
||||
if (auto in = indices.find(i->owner.lock().get()); in != indices.end()) { // only connections within the collection
|
||||
QCborArray c;
|
||||
c << idx;
|
||||
c << Util::enumName(o->dataType());
|
||||
c << o->index;
|
||||
c << in->second;
|
||||
c << Util::enumName(i->dataType());
|
||||
c << i->index;
|
||||
cm << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.insert(QString("connections"), cm);
|
||||
}
|
||||
|
||||
{ /* center */ } {
|
||||
int count = 0;
|
||||
QPoint center;
|
||||
for (auto n : v) {
|
||||
if (n->isVolatile()) continue;
|
||||
center += QPoint(n->x, n->y);
|
||||
count++;
|
||||
}
|
||||
center /= count;
|
||||
m.insert(QString("center"), QCborArray({center.x(), center.y()}));
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborMap& m, std::shared_ptr<Graph> parent, QPoint cp) {
|
||||
std::vector<std::shared_ptr<Node>> v;
|
||||
|
||||
QPoint center;
|
||||
|
||||
{ /* center point */ } {
|
||||
QCborArray c = m.value("center").toArray();
|
||||
center.setX(static_cast<int>(c.at(0).toInteger()));
|
||||
center.setY(static_cast<int>(c.at(1).toInteger()));
|
||||
}
|
||||
|
||||
{ /* nodes */ } {
|
||||
QCborArray n = m.value("nodes").toArray();
|
||||
v.reserve(static_cast<size_t>(n.size()));
|
||||
for (auto nm : n) v.push_back(Node::fromCbor(nm, parent));
|
||||
}
|
||||
|
||||
{ /* connections */ } {
|
||||
QCborArray cn = m.value("connections").toArray();
|
||||
|
||||
auto pmt = QMetaType::metaObjectForType(QMetaType::type("Xybrid::Data::Port"));
|
||||
auto dtm = pmt->enumerator(pmt->indexOfEnumerator("DataType"));
|
||||
|
||||
for (auto cv : cn) {
|
||||
auto c = cv.toArray();
|
||||
if (c.empty()) continue;
|
||||
auto on = v[static_cast<size_t>(c[0].toInteger())];
|
||||
auto in = v[static_cast<size_t>(c[3].toInteger())];
|
||||
auto op = on->port(Port::Output, static_cast<Port::DataType>(dtm.keyToValue(c[1].toString().toStdString().c_str())), static_cast<uint8_t>(c[2].toInteger()));
|
||||
auto ip = in->port(Port::Input, static_cast<Port::DataType>(dtm.keyToValue(c[4].toString().toStdString().c_str())), static_cast<uint8_t>(c[5].toInteger()));
|
||||
if (op && ip) op->connect(ip);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cp.isNull()) { // offset and such
|
||||
QPoint off = cp - center;
|
||||
for (auto n : v) {
|
||||
n->x += off.x();
|
||||
n->y += off.y();
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
std::vector<std::shared_ptr<Node>> Node::multiFromCbor(const QCborValue& m, std::shared_ptr<Graph> parent, QPoint cp) { return multiFromCbor(m.toMap(), parent, cp); }
|
||||
|
||||
void Node::parentTo(std::shared_ptr<Graph> graph) {
|
||||
auto t = shared_from_this(); // keep alive during reparenting
|
||||
if (auto p = parent.lock(); p) {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <QMutex>
|
||||
#include <QPointer>
|
||||
#include <QObject>
|
||||
#include <QPoint>
|
||||
|
||||
class QPainter;
|
||||
class QStyleOptionGraphicsItem;
|
||||
|
@ -98,10 +99,13 @@ namespace Xybrid::Data {
|
|||
Node() = default;
|
||||
virtual ~Node() = default;
|
||||
|
||||
QCborMap toCbor() const;
|
||||
static std::shared_ptr<Node> fromCbor(const QCborMap&, std::shared_ptr<Graph> = nullptr);
|
||||
static std::shared_ptr<Node> fromCbor(const QCborValue&, std::shared_ptr<Graph> = nullptr);
|
||||
|
||||
QCborMap toCbor() const;
|
||||
static QCborMap multiToCbor(std::vector<std::shared_ptr<Node>>&);
|
||||
static std::vector<std::shared_ptr<Node>> multiFromCbor(const QCborMap&, std::shared_ptr<Graph> = nullptr, QPoint = QPoint());
|
||||
static std::vector<std::shared_ptr<Node>> multiFromCbor(const QCborValue&, std::shared_ptr<Graph> = nullptr, QPoint = QPoint());
|
||||
|
||||
void parentTo(std::shared_ptr<Graph>);
|
||||
|
||||
|
@ -116,11 +120,13 @@ namespace Xybrid::Data {
|
|||
|
||||
virtual void init() { }
|
||||
virtual void reset() { }
|
||||
virtual void process() { }
|
||||
virtual void saveData(QCborMap&) const { }
|
||||
virtual void loadData(const QCborMap&) { }
|
||||
|
||||
virtual void process() { }
|
||||
virtual std::string pluginName() const;
|
||||
/// If true, excluded from copy/paste operations.
|
||||
virtual bool isVolatile() const { return false; }
|
||||
|
||||
virtual void onGadgetCreated() { }
|
||||
virtual void drawCustomChrome(QPainter*, const QStyleOptionGraphicsItem*) { }
|
||||
|
|
|
@ -468,6 +468,8 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) {
|
|||
void MainWindow::openGraph(const std::shared_ptr<Data::Graph>& g) {
|
||||
if (!g) return; // invalid
|
||||
QPointF scrollPt(g->viewX, g->viewY);
|
||||
delete ui->patchboardView->scene();
|
||||
//ui->patchboardView->setScene(nullptr);
|
||||
ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g));
|
||||
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 0);
|
||||
QScroller::scroller(ui->patchboardView)->scrollTo(scrollPt, 1);
|
||||
|
|
|
@ -17,6 +17,8 @@ namespace Xybrid::Gadgets {
|
|||
|
||||
void setPort(Data::Port::Type type, Data::Port::DataType dataType, uint8_t index);
|
||||
|
||||
bool isVolatile() const override { return true; }
|
||||
|
||||
void process() override;
|
||||
|
||||
void onRename() override;
|
||||
|
|
|
@ -197,7 +197,7 @@ void NodeObject::setGadgetSize(QPointF p) {
|
|||
|
||||
void NodeObject::promptDelete() {
|
||||
QPointer<NodeObject> t = this;
|
||||
if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove node? (This cannot be undone!)"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
|
||||
if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove node? (This cannot be undone!)"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
|
||||
if (t) {
|
||||
t->node->parentTo(nullptr); // unparent node so it gets deleted
|
||||
delete t;
|
||||
|
|
|
@ -11,6 +11,16 @@ using Xybrid::UI::PatchboardScene;
|
|||
#include <QMenu>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QShortcut>
|
||||
#include <QGuiApplication>
|
||||
#include <QClipboard>
|
||||
#include <QMimeData>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <QCborValue>
|
||||
#include <QCborMap>
|
||||
#include <QCborArray>
|
||||
|
||||
#include <QGraphicsSceneContextMenuEvent>
|
||||
|
||||
#include "data/graph.h"
|
||||
|
@ -29,6 +39,12 @@ using namespace Xybrid::Audio;
|
|||
|
||||
#include "config/colorscheme.h"
|
||||
|
||||
QShortcut* PatchboardScene::shortcut(QKeySequence s) {
|
||||
auto c = new QShortcut(s, view);
|
||||
connect(this, &QObject::destroyed, c, [c] {delete c;});
|
||||
return c;
|
||||
}
|
||||
|
||||
PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xybrid::Data::Graph>& g) : QGraphicsScene(parent) {
|
||||
graph = g;
|
||||
view = parent;
|
||||
|
@ -39,6 +55,73 @@ PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr<Xy
|
|||
connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::queueResize);
|
||||
connect(this, &QGraphicsScene::changed, this, &PatchboardScene::queueResize);
|
||||
|
||||
auto copy = [this] {
|
||||
auto sel = this->selectedItems();
|
||||
if (sel.empty()) return; // nope
|
||||
|
||||
auto* clip = QGuiApplication::clipboard();
|
||||
auto* data = new QMimeData();
|
||||
|
||||
std::vector<std::shared_ptr<Node>> v;
|
||||
v.reserve(static_cast<size_t>(sel.size()));
|
||||
QRectF r;
|
||||
for (auto s : sel) {
|
||||
auto n = static_cast<NodeObject*>(s)->getNode();
|
||||
if (n->isVolatile()) continue;
|
||||
v.push_back(n);
|
||||
r = r.united(s->sceneBoundingRect());
|
||||
}
|
||||
|
||||
auto cbor = Node::multiToCbor(v);
|
||||
|
||||
{ /* override center point */ } {
|
||||
auto c = r.center().toPoint();
|
||||
cbor[QString("center")] = QCborArray({c.x(), c.y()});
|
||||
}
|
||||
|
||||
data->setData("xybrid-internal/x-graph-copy", cbor.toCborValue().toCbor());
|
||||
clip->setMimeData(data);
|
||||
};
|
||||
connect(shortcut(QKeySequence(QKeySequence::StandardKey::Delete)), &QShortcut::activated, this, [this] {
|
||||
auto sel = this->selectedItems();
|
||||
if (sel.empty()) return;
|
||||
if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove %1 node(s)? (This cannot be undone!)").arg(sel.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
|
||||
for (auto n : sel) {
|
||||
if (!n) continue;
|
||||
static_cast<NodeObject*>(n)->getNode()->parentTo(nullptr);
|
||||
delete n;
|
||||
}
|
||||
});
|
||||
connect(shortcut(QKeySequence(QKeySequence::StandardKey::Cut)), &QShortcut::activated, this, [this, copy] {
|
||||
auto sel = this->selectedItems();
|
||||
if (sel.empty()) return;
|
||||
copy();
|
||||
if (QMessageBox::warning(nullptr, "Are you sure?", QString("Remove %1 node(s)? (This cannot be undone!)").arg(sel.size()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) != QMessageBox::Yes) return;
|
||||
for (auto n : sel) {
|
||||
if (!n) continue;
|
||||
static_cast<NodeObject*>(n)->getNode()->parentTo(nullptr);
|
||||
delete n;
|
||||
}
|
||||
});
|
||||
connect(shortcut(QKeySequence(QKeySequence::StandardKey::Copy)), &QShortcut::activated, this, [copy] { copy(); });
|
||||
connect(shortcut(QKeySequence(QKeySequence::StandardKey::Paste)), &QShortcut::activated, this, [this] {
|
||||
const auto* clip = QGuiApplication::clipboard();
|
||||
const auto* data = clip->mimeData();
|
||||
|
||||
if (!data->hasFormat("xybrid-internal/x-graph-copy")) return; // no pattern data
|
||||
|
||||
auto center = view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect().center().toPoint();
|
||||
|
||||
auto v = Node::multiFromCbor(QCborValue::fromCbor(data->data("xybrid-internal/x-graph-copy")), graph, center);
|
||||
|
||||
setSelectionArea(QPainterPath()); // deselect all
|
||||
for (auto n : v) { // and select pasted objects
|
||||
auto o = new NodeObject(n);
|
||||
addItem(o);
|
||||
o->setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
|
||||
class QShortcut;
|
||||
|
||||
namespace Xybrid::Data { class Graph; }
|
||||
|
||||
namespace Xybrid::UI {
|
||||
|
@ -22,6 +24,8 @@ namespace Xybrid::UI {
|
|||
void startPreview(int, int16_t);
|
||||
void stopPreview(int);
|
||||
|
||||
QShortcut* shortcut(QKeySequence);
|
||||
|
||||
public:
|
||||
PatchboardScene(QGraphicsView* view, const std::shared_ptr<Data::Graph>& graph);
|
||||
~PatchboardScene() override = default;
|
||||
|
|
Loading…
Reference in New Issue