diff --git a/asset-work/xybrid-logo-tiny.xcf b/asset-work/xybrid-logo-tiny.xcf new file mode 100644 index 0000000..463243c Binary files /dev/null and b/asset-work/xybrid-logo-tiny.xcf differ diff --git a/asset-work/xybrid-logo.xcf b/asset-work/xybrid-logo.xcf new file mode 100644 index 0000000..cda0086 Binary files /dev/null and b/asset-work/xybrid-logo.xcf differ diff --git a/notes b/notes index 3d370a1..e8684e0 100644 --- a/notes +++ b/notes @@ -35,7 +35,7 @@ project data { x note-on events send the actual note as a float value - nope, separate event for cents (bcd? that would futz with interpolation though... signed byte, -100..100) - treat port FF as global control?? { + unique port for globals (-2 internally, styled as (G), and placed by, get this, pressing g) { what to do with the notes? tXX - tempo (second tXX as high byte, .XX for fine tempo (0..100)) > anything else? @@ -50,11 +50,23 @@ TODO { - strut command in pattern editor (mostly selection agnostic) } group 2 { - skeleton graph and node - data namespace I guess + - skeleton graph and node + - skeleton audio engine skeleton plugin registry - stuff into config namespace? - skeleton audio engine } + for passthrough { + passthroughLink attribute to keep track of whether a passthrough exists + ^ build the logic into output pull()? + gadget that links up to parent's things + maybe call Graph::process as the end of its own subqueue?? + } + ... how to handle graph port naming and numbering? + have them fully dependent on the internal gadget?? + - maybe make inputs/outputs into an (ordered) map of maps + + audio engine invokes workers, then QThread::wait()s on them + # fix how qt5.12 broke header text (removed elide for now) add metadata and pattern properties (artist, song title, project bpm; pattern name, length etc.) diff --git a/xybrid/audio/audioengine.cpp b/xybrid/audio/audioengine.cpp index 9359181..6b12daa 100644 --- a/xybrid/audio/audioengine.cpp +++ b/xybrid/audio/audioengine.cpp @@ -105,7 +105,7 @@ void AudioEngine::play(std::shared_ptr p) { //tickId = 0; // actually, no reason to reset this mode = Playing; - emit this->playbackModeChanged(mode); + emit this->playbackModeChanged(); }, Qt::QueuedConnection); } @@ -114,7 +114,7 @@ void AudioEngine::stop() { project = nullptr; deinitAudio(); mode = Stopped; - emit this->playbackModeChanged(mode); + emit this->playbackModeChanged(); }, Qt::QueuedConnection); } @@ -192,6 +192,17 @@ void AudioEngine::nextTick() { if (!p || curRow >= p->rows) advanceSeq(); MainWindow* w = project->socket->window; QMetaObject::invokeMethod(w, [this, w]{ w->playbackPosition(seqPos, curRow); }, Qt::QueuedConnection); + + // process global commands first + for (int c = 0; c < static_cast(p->numChannels()); c++) { + if (auto& row = p->rowAt(c, curRow); row.port == -2 && row.params) { + for (auto p : *row.params) { + if (p[0] == 't' && p[1] > 0) tempo = p[1]; + } + } + } + + // TODO then assemble command buffers }; curTick++; diff --git a/xybrid/audio/audioengine.h b/xybrid/audio/audioengine.h index 6eba7e5..05b91a3 100644 --- a/xybrid/audio/audioengine.h +++ b/xybrid/audio/audioengine.h @@ -70,7 +70,7 @@ namespace Xybrid::Audio { volatile int note = 12*5; signals: - void playbackModeChanged(PlaybackMode); + void playbackModeChanged(); public slots: }; diff --git a/xybrid/config/pluginregistry.cpp b/xybrid/config/pluginregistry.cpp new file mode 100644 index 0000000..d0fc60f --- /dev/null +++ b/xybrid/config/pluginregistry.cpp @@ -0,0 +1,59 @@ +#include "pluginregistry.h" +using namespace Xybrid::Config; + +#include +#include +#include + +#include + +#include "data/node.h" +using namespace Xybrid::Data; + +namespace { + typedef std::list> fqueue; // typedef so QtCreator's auto indent doesn't completely break :| + fqueue& regQueue() { + static fqueue q; + return q; + } + bool& initialized() { static bool b = false; return b; } + + std::unordered_map> plugins; +} + +bool PluginRegistry::enqueueRegistration(std::function f) { + auto& queue = regQueue(); + queue.push_back(f); + if (initialized()) f(); + return true; +} + +void PluginRegistry::init() { + if (initialized()) return; + + for (auto& f : regQueue()) f(); + initialized() = true; +} + +void PluginRegistry::registerPlugin(std::shared_ptr pi) { + if (pi->id.empty()) return; + if (plugins.find(pi->id) != plugins.end()) return; + plugins[pi->id] = pi; +} + +std::shared_ptr PluginRegistry::createInstance(const std::string& id) { + auto f = plugins.find(id); + if (f == plugins.end()) return nullptr; + auto n = f->second->createInstance(); + n->plugin = f->second; + return n; +} + +void PluginRegistry::populatePluginMenu(QMenu* m, std::function)> f) { + for (auto& i : plugins) m->addAction(QString::fromStdString(i.second->displayName), [f, pi = i.second] { + auto n = pi->createInstance(); + n->plugin = pi; + f(n); + }); + +} diff --git a/xybrid/config/pluginregistry.h b/xybrid/config/pluginregistry.h new file mode 100644 index 0000000..29fdbcb --- /dev/null +++ b/xybrid/config/pluginregistry.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class QMenu; + +namespace Xybrid::Data { + class Node; +} + +namespace Xybrid::Config { + class PluginInfo { + public: + std::string id; + std::string displayName; + std::string category; + std::function()> createInstance; + + PluginInfo() = default; + virtual ~PluginInfo() = default; + }; + + namespace PluginRegistry { + bool enqueueRegistration(std::function); + void registerPlugin(std::shared_ptr); + void init(); + + std::shared_ptr createInstance(const std::string& id); + void populatePluginMenu(QMenu*, std::function)>); + } +} diff --git a/xybrid/data/graph.cpp b/xybrid/data/graph.cpp new file mode 100644 index 0000000..048ab0d --- /dev/null +++ b/xybrid/data/graph.cpp @@ -0,0 +1,17 @@ +#include "graph.h" +using namespace Xybrid::Data; + +#include "config/pluginregistry.h" +using namespace Xybrid::Config; + +namespace { + bool c = PluginRegistry::enqueueRegistration([] { + auto i = std::make_shared(); + i->id = "graph"; + i->displayName = "Subgraph"; + i->createInstance = []{ return std::make_shared(); }; + PluginRegistry::registerPlugin(i); + }); +} + +//std::string Graph::pluginName() const { return "Subgraph"; } diff --git a/xybrid/data/graph.h b/xybrid/data/graph.h index 8f3812e..923cc9c 100644 --- a/xybrid/data/graph.h +++ b/xybrid/data/graph.h @@ -6,5 +6,10 @@ namespace Xybrid::Data { class Graph : public Node { public: std::vector> children; + + // position of viewport within graph (not serialized) + int viewX{}, viewY{}; + + //std::string pluginName() const override; }; } diff --git a/xybrid/data/node.cpp b/xybrid/data/node.cpp index 36fa960..28203a5 100644 --- a/xybrid/data/node.cpp +++ b/xybrid/data/node.cpp @@ -1,19 +1,31 @@ #include "node.h" -using Xybrid::Data::Node; -using Xybrid::Data::Port; +using namespace Xybrid::Data; #include "data/graph.h" +#include "data/porttypes.h" + +#include "config/pluginregistry.h" #include -bool Port::canConnectTo(DataType d) { +#include + +Port::~Port() { + // clean up others' connection lists + while (connections.size() > 0) { + if (auto cc = connections.back().lock(); cc) cc->cleanConnections(); + connections.pop_back(); + } +} + +bool Port::canConnectTo(DataType d) const { return d == dataType(); } bool Port::connect(std::shared_ptr p) { if (!p) return false; // no blank pointers pls // actual processing is always done on the input port, since that's where any limits are - if (type == Port::Output) return p->type == Port::Input && p->connect(shared_from_this()); + if (type == Output) return p->type == Input && p->connect(shared_from_this()); if (!canConnectTo(p->dataType())) return false; // can't hook up to an incompatible data type for (auto c : connections) if (c.lock() == p) return true; // I guess report success if already connected? if (singleInput() && connections.size() > 0) return false; // reject multiple connections on single-input ports @@ -30,6 +42,17 @@ void Port::disconnect(std::shared_ptr p) { p->connections.erase(std::remove_if(p->connections.begin(), p->connections.end(), [t](auto w) { return w.lock() == t; }), p->connections.end()); } +void Port::cleanConnections() { + connections.erase(std::remove_if(connections.begin(), connections.end(), [](auto w) { return !w.lock(); }), connections.end()); +} + +std::shared_ptr Port::makePort(DataType dt) { + if (dt == Audio) return std::make_shared(); + if (dt == Command) return std::make_shared(); + // fallback + return std::make_shared(); +} + void Node::parentTo(std::shared_ptr graph) { auto t = shared_from_this(); // keep alive during reparenting if (auto p = parent.lock(); p) { @@ -40,3 +63,34 @@ void Node::parentTo(std::shared_ptr graph) { graph->children.push_back(t); } } + +std::shared_ptr Node::port(Port::Type t, Port::DataType dt, uint8_t idx, bool addIfNeeded) { + auto& m = t == Port::Input ? inputs : outputs; + if (auto mdt = m.find(dt); mdt != m.end()) { + if (auto it = mdt->second.find(idx); it != mdt->second.end()) return it->second; + } + return addIfNeeded ? addPort(t, dt, idx) : nullptr; +} + +std::shared_ptr Node::addPort(Port::Type t, Port::DataType dt, uint8_t idx) { + auto& m = t == Port::Input ? inputs : outputs; + m.try_emplace(dt); + auto mdt = m.find(dt); + if (mdt->second.find(idx) == mdt->second.end()) { + auto p = Port::makePort(dt); + p->type = t; + mdt->second.insert({idx, p}); + p->index = idx; + return p; + } + return nullptr; +} + +void Node::removePort(Port::Type t, Port::DataType dt, uint8_t idx) { + auto& m = t == Port::Input ? inputs : outputs; + if (auto mdt = m.find(dt); mdt != m.end()) { + mdt->second.erase(idx); + } +} + +std::string Node::pluginName() const { if (!plugin) return "(unknown plugin)"; return plugin->displayName; } diff --git a/xybrid/data/node.h b/xybrid/data/node.h index 512cf37..90de681 100644 --- a/xybrid/data/node.h +++ b/xybrid/data/node.h @@ -2,35 +2,54 @@ #include #include +#include #include +#include + +namespace Xybrid::UI { + class PortObject; +} + +namespace Xybrid::Config { + class PluginInfo; +} + namespace Xybrid::Data { class Graph; class Node; class Port : public std::enable_shared_from_this { public: - enum Type : char { + enum Type : uint8_t { Input, Output }; - enum DataType : char { - Command, MIDI, Audio, Parameter + enum DataType : uint8_t { + Audio, Command, MIDI, Parameter }; std::weak_ptr owner; std::vector> connections; std::weak_ptr passthroughFor; Type type; // TODO: figure out passthrough? + uint8_t index; size_t tickUpdatedOn = static_cast(-1); - virtual ~Port() = default; + QPointer obj; - virtual DataType dataType(); - virtual bool singleInput() { return false; } - virtual bool canConnectTo(DataType); + std::string name; + + virtual ~Port(); + + virtual DataType dataType() const { return static_cast(-1); } + virtual bool singleInput() const { return false; } + virtual bool canConnectTo(DataType) const; /*virtual*/ bool connect(std::shared_ptr); /*virtual*/ void disconnect(std::shared_ptr); + void cleanConnections(); - virtual void pull(); // make sure data for this tick is available + virtual void pull() { } // make sure data for this tick is available + + static std::shared_ptr makePort(DataType); }; class Node : public std::enable_shared_from_this { @@ -39,12 +58,19 @@ namespace Xybrid::Data { int x{}, y{}; std::string name; - std::vector> inputs, outputs; + std::map>> inputs, outputs; + + std::shared_ptr plugin; virtual ~Node() = default; void parentTo(std::shared_ptr); + std::shared_ptr port(Port::Type, Port::DataType, uint8_t, bool addIfNeeded = false); + std::shared_ptr addPort(Port::Type, Port::DataType, uint8_t); + void removePort(Port::Type, Port::DataType, uint8_t); + virtual void process() { } + virtual std::string pluginName() const; }; } diff --git a/xybrid/data/porttypes.h b/xybrid/data/porttypes.h new file mode 100644 index 0000000..65edacd --- /dev/null +++ b/xybrid/data/porttypes.h @@ -0,0 +1,23 @@ +#pragma once + +#include "data/node.h" + +namespace Xybrid::Data { + class AudioPort : public Port { + + public: + AudioPort() = default; + ~AudioPort() override = default; + + Port::DataType dataType() const override { return Port::Audio; } + }; + + class CommandPort : public Port { + + public: + CommandPort() = default; + ~CommandPort() override = default; + + Port::DataType dataType() const override { return Port::Command; } + }; +} diff --git a/xybrid/main.cpp b/xybrid/main.cpp index 3da4a79..ab526dd 100644 --- a/xybrid/main.cpp +++ b/xybrid/main.cpp @@ -1,18 +1,26 @@ #include "mainwindow.h" #include "audio/audioengine.h" +#include "config/pluginregistry.h" #include #include #include #include +#include int main(int argc, char *argv[]) { QApplication a(argc, argv); + // enable antialiasing on accelerated graphicsview + QSurfaceFormat fmt; + fmt.setSamples(10); + QSurfaceFormat::setDefaultFormat(fmt); + // make sure bundled fonts are loaded QFontDatabase::addApplicationFont(":/fonts/iosevka-term-light.ttf"); + Xybrid::Config::PluginRegistry::init(); Xybrid::Audio::AudioEngine::init(); auto* w = new Xybrid::MainWindow(); diff --git a/xybrid/mainwindow.cpp b/xybrid/mainwindow.cpp index f6a99ff..847df0f 100644 --- a/xybrid/mainwindow.cpp +++ b/xybrid/mainwindow.cpp @@ -11,27 +11,44 @@ using Xybrid::MainWindow; #include #include #include +#include +#include + +#include +#include + +#include "data/graph.h" #include "util/strings.h" +#include "util/lambdaeventfilter.h" #include "fileops.h" #include "ui/patternlistmodel.h" #include "ui/patternsequencermodel.h" #include "ui/patterneditoritemdelegate.h" +#include "ui/patchboard/patchboardscene.h" + #include "editing/projectcommands.h" +#include "config/pluginregistry.h" #include "audio/audioengine.h" using Xybrid::Data::Project; using Xybrid::Data::Pattern; +using Xybrid::Data::Graph; +using Xybrid::Data::Node; +using Xybrid::Data::Port; using Xybrid::UI::PatternListModel; using Xybrid::UI::PatternSequencerModel; using Xybrid::UI::PatternEditorModel; using Xybrid::UI::PatternEditorItemDelegate; +using Xybrid::UI::PatchboardScene; + using namespace Xybrid::Editing; +using namespace Xybrid::Config; using namespace Xybrid::Audio; namespace { @@ -64,7 +81,7 @@ MainWindow::MainWindow(QWidget *parent) : auto* t = ui->tabWidget; t->setCornerWidget(ui->menuBar); - t->setCornerWidget(ui->label, Qt::TopLeftCorner); + t->setCornerWidget(ui->logo, Qt::TopLeftCorner); //ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }"); // prevent right pane of pattern view from being collapsed @@ -214,6 +231,49 @@ MainWindow::MainWindow(QWidget *parent) : */ } + { /* Set up patchboard view */ } { + //ui->patchboardView->setDragMode(QGraphicsView::DragMode::RubberBandDrag); + auto* view = ui->patchboardView; + + view->setViewport(new QOpenGLWidget); // enable hardware acceleration + view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing); + + view->setAlignment(Qt::AlignTop | Qt::AlignLeft); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setAttribute(Qt::WA_AcceptTouchEvents, true); + QScroller::grabGesture(view, QScroller::MiddleMouseButtonGesture); + { + auto prop = QScroller::scroller(view)->scrollerProperties(); + prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + prop.setScrollMetric(QScrollerProperties::AxisLockThreshold, 1); + QScroller::scroller(view)->setScrollerProperties(prop); + QScroller::scroller(view)->setSnapPositionsX({}); + QScroller::scroller(view)->setSnapPositionsY({}); + } + + // event filter to make drag-to-select only happen on left click + view->viewport()->installEventFilter(new LambdaEventFilter(view, [view](QObject* w, QEvent* e) { + if (e->type() == QEvent::MouseButtonPress) { + auto* me = static_cast(e); + // initiate drag + if (me->button() == Qt::LeftButton) view->setDragMode(QGraphicsView::RubberBandDrag); + } else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end + QTimer::singleShot(1, [view] { view->setDragMode(QGraphicsView::NoDrag); }); + } + return w->QObject::eventFilter(w, e); + })); + + //view->setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu); + + /*connect(view, &QGraphicsView::customContextMenuRequested, this, [this, view] { + qDebug() << "context"; + //view->viewport->visibleRegion().boundingRect().topLeft() + });*/ + + } + // Set up signaling from project to UI socket = new UISocket(); socket->setParent(this); @@ -233,7 +293,7 @@ MainWindow::MainWindow(QWidget *parent) : }); // and from audio engine - connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction](AudioEngine::PlaybackMode) { + connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction]() { bool locked = project->editingLocked(); undoAction->setEnabled(!locked); redoAction->setEnabled(!locked); @@ -268,6 +328,24 @@ void MainWindow::menuFileNew() { project = std::make_shared(); project->sequence.push_back(project->newPattern().get()); + // TEMP add some stuff + { + auto g1 = PluginRegistry::createInstance("graph");//std::make_shared(); + g1->parentTo(project->rootGraph); + g1->x = 64; + g1->y = 64; + g1->addPort(Port::Input, Port::Command, 0); + + auto g2 = PluginRegistry::createInstance("graph"); + g2->parentTo(project->rootGraph); + g2->x = 444; + g2->y = 22; + g1->addPort(Port::Output, Port::Audio, 0)->connect(g2->addPort(Port::Input, Port::Audio, 0)); + g2->addPort(Port::Input, Port::Audio, 1); + g2->addPort(Port::Input, Port::Audio, 2)->name = "Named port"; + } + // + onNewProjectLoaded(); } @@ -310,6 +388,8 @@ void MainWindow::onNewProjectLoaded() { break; } + openGraph(project->rootGraph); + updateTitle(); } @@ -367,3 +447,8 @@ bool MainWindow::selectPatternForEditing(Pattern* pattern) { return true; } + +void MainWindow::openGraph(const std::shared_ptr& g) { + if (!g) return; // invalid + ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g)); +} diff --git a/xybrid/mainwindow.h b/xybrid/mainwindow.h index 5f63448..6df76c4 100644 --- a/xybrid/mainwindow.h +++ b/xybrid/mainwindow.h @@ -32,6 +32,8 @@ namespace Xybrid { void updatePatternLists(); bool selectPatternForEditing(Data::Pattern*); + void openGraph(const std::shared_ptr&); + void updateTitle(); public: diff --git a/xybrid/mainwindow.ui b/xybrid/mainwindow.ui index ed27ffc..b844f7c 100644 --- a/xybrid/mainwindow.ui +++ b/xybrid/mainwindow.ui @@ -33,7 +33,7 @@ Qt::NoFocus - 0 + 1 true @@ -249,7 +249,11 @@ 0 - + + + QGraphicsView::FullViewportUpdate + + @@ -257,17 +261,20 @@ nonexistent - + 10 10 - 63 - 16 + 81 + 31 - (logo here) + + + + :/img/xybrid-logo-tiny.png @@ -366,7 +373,9 @@
ui/patterneditorview.h
- + + + actionNew diff --git a/xybrid/res/resources.qrc b/xybrid/res/resources.qrc index a1b99d3..1dcfd74 100644 --- a/xybrid/res/resources.qrc +++ b/xybrid/res/resources.qrc @@ -2,4 +2,7 @@ iosevka-term-light.ttf + + xybrid-logo-tiny.png + diff --git a/xybrid/res/xybrid-logo-tiny.png b/xybrid/res/xybrid-logo-tiny.png new file mode 100644 index 0000000..6cdcd2c Binary files /dev/null and b/xybrid/res/xybrid-logo-tiny.png differ diff --git a/xybrid/ui/patchboard/nodeobject.cpp b/xybrid/ui/patchboard/nodeobject.cpp new file mode 100644 index 0000000..419be8f --- /dev/null +++ b/xybrid/ui/patchboard/nodeobject.cpp @@ -0,0 +1,375 @@ +#include "nodeobject.h" +using Xybrid::UI::NodeObject; +using Xybrid::UI::PortObject; +using Xybrid::UI::PortConnectionObject; +using Xybrid::Data::Node; +using Xybrid::Data::Port; + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + const constexpr qreal portSize [[maybe_unused]] = 10; + const constexpr qreal portSpacing [[maybe_unused]] = 3; + + const QColor tcolor[] { + QColor(239, 179, 59), // Audio + QColor(163, 95, 191), // Command + QColor(95, 191, 163), // MIDI + QColor(127, 127, 255), // Parameter + }; + + const QString tname[] { + "Audio", "Command", "MIDI", "Parameter" + }; +} + +void PortObject::connectTo(Xybrid::UI::PortObject* o) { + if (!o) return; + if (connections.find(o) != connections.end()) return; + if (port->type == o->port->type) return; + + PortObject* in; + PortObject* out; + if (port->type == Port::Input) { in = this; out = o; } + else { out = this; in = o; } + + if (out->port->connect(in->port)) { + /*auto* pc =*/ new PortConnectionObject(in, out); + } + +} + +void PortObject::setHighlighted(bool h, bool hideLabel) { + highlighted = h; + + bool lv = h && !hideLabel; + if (lv) { + QString txt = QString("%1 %2").arg(tname[port->dataType()].toLower()).arg(QString::number(port->index)); + if (!port->name.empty()) txt = QString("%1 (%2)").arg(QString::fromStdString(port->name)).arg(txt); + QColor c = tcolor[port->dataType()]; + label->setText(txt); + label->setBrush(c); + + labelShadow->setText(txt); + labelShadow->setBrush(c.darker(400)); + labelShadow->setPen(QPen(labelShadow->brush(), 2.5)); + + auto lbr = label->boundingRect(); + if (port->type == Port::Input) label->setPos(QPointF(-lbr.width() - (portSize/2 + portSpacing), lbr.height() * -.5)); + else label->setPos(QPointF(portSize/2 + portSpacing, lbr.height() * -.5)); + auto lbsr = labelShadow->boundingRect(); + labelShadow->setPos(label->pos() + (lbr.bottomRight() - lbsr.bottomRight()) / 2); + } + label->setVisible(lv); + labelShadow->setVisible(lv); + + update(); +} + +PortObject::PortObject(const std::shared_ptr& p) { + port = p; + p->obj = this; + setAcceptHoverEvents(true); + setAcceptedMouseButtons(Qt::LeftButton); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + + labelShadow = new QGraphicsSimpleTextItem(this); + labelShadow->setVisible(false); + label = new QGraphicsSimpleTextItem(this); + label->setVisible(false); + + for (auto c : port->connections) { + if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj); + } +} + +PortObject::~PortObject() { + while (connections.begin() != connections.end()) delete connections.begin()->second; +} + +void PortObject::mousePressEvent(QGraphicsSceneMouseEvent*) { + setCursor(Qt::ClosedHandCursor); + setHighlighted(true, true); + dragLine.reset(new QGraphicsLineItem()); + dragLine->setPen(QPen(tcolor[port->dataType()].lighter(125), 1.5)); + dragLine->setLine(QLineF(scenePos(), scenePos())); + dragLine->setZValue(100); + scene()->addItem(dragLine.get()); +} + +void PortObject::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) { + unsetCursor(); + dragLine.reset(); + + auto* i = scene()->itemAt(e->scenePos(), QTransform()); + if (i && i->type() == PortObject::Type) { + auto* p = static_cast(i); + //qDebug() << "connection:" << port->connect(p->port); + connectTo(p); + } +} + +void PortObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) { + if (dragLine) dragLine->setLine(QLineF(scenePos(), e->scenePos())); + update(); +} + +void PortObject::hoverEnterEvent(QGraphicsSceneHoverEvent*) { + setHighlighted(true); +} + +void PortObject::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { + setHighlighted(false); +} + +void PortObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) { + auto* m = new QMenu(); + m->addAction("Disconnect All", this, [this] { + while (connections.begin() != connections.end()) { + auto* c = connections.begin()->second; + c->in->port->disconnect(c->out->port); + delete c; + } + });//->setEnabled(this->connections.size() != 0); + m->popup(e->screenPos()); +} + +void PortObject::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) { + QColor bg = tcolor[port->dataType()]; + + QColor outline = bg.darker(200); + if (highlighted) outline = bg.lighter(150); + + painter->setRenderHint(QPainter::RenderHint::Antialiasing); + painter->setBrush(QBrush(bg)); + painter->setPen(QPen(QBrush(outline), 1)); + painter->drawEllipse(boundingRect()); +} + +QRectF PortObject::boundingRect() const { + return QRectF(portSize * -.5, portSize * -.5, portSize, portSize); +} + +NodeObject::NodeObject(const std::shared_ptr& n) { + node = n; + + setFlag(QGraphicsItem::ItemIsMovable); + setFlag(QGraphicsItem::ItemIsFocusable); + setFlag(QGraphicsItem::ItemIsSelectable); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + + /*auto* t = new QGraphicsTextItem(QString::fromStdString(n->name), this); + t->setPos(4, 4); + t->setFlag(QGraphicsItem::ItemIsSelectable, false);*/ + //t->setPen(QPen(QColor(0, 0, 0), 0.25)); + //t->setBrush(QBrush(QColor(255, 255, 255))); + + setPos(node->x, node->y); + + connect(this, &QGraphicsObject::xChanged, this, &NodeObject::onMoved); + connect(this, &QGraphicsObject::yChanged, this, &NodeObject::onMoved); + + createPorts(); +} + +void NodeObject::promptDelete() { + QPointer 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 (t) { + t->node->parentTo(nullptr); // unparent node so it gets deleted + delete t; + } +} + +void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) { + auto* m = new QMenu(); + m->addAction("Delete node", this, &NodeObject::promptDelete); + m->popup(e->screenPos()); +} + +void NodeObject::bringToTop(bool force) { + for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this); +} + +void NodeObject::createPorts() { + inputPortContainer.reset(new QGraphicsRectItem(this)); + outputPortContainer.reset(new QGraphicsRectItem(this)); + updateGeometry(); + + QPointF cursor = QPointF(0, 0); + for (auto mdt : node->inputs) { + for (auto pp : mdt.second) { + auto* p = new PortObject(pp.second); + p->setParentItem(inputPortContainer.get()); + p->setPos(cursor); + cursor += QPointF(0, portSize + portSpacing); + } + } + + cursor = QPointF(0, 0); + for (auto mdt : node->outputs) { + for (auto pp : mdt.second) { + auto* p = new PortObject(pp.second); + p->setParentItem(outputPortContainer.get()); + p->setPos(cursor); + cursor += QPointF(0, portSize + portSpacing); + } + } + +} + +void NodeObject::updateGeometry() { + if (inputPortContainer) inputPortContainer->setPos(QPointF(portSize * -.5 - portSpacing, portSize)); + if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + portSize * .5 + portSpacing, portSize)); +} + +void NodeObject::onMoved() { + if (x() < 0) setX(0); + else setX(std::round(x())); + if (y() < 0) setY(0); + else setY(std::round(y())); + node->x = static_cast(x()); + node->y = static_cast(y()); + + if (isSelected()) { + bringToTop(); + } + + if (auto s = scene(); s) s->update(); +} + +void NodeObject::focusInEvent(QFocusEvent *) { + bringToTop(true); +} + +void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) { + QRectF r = boundingRect(); + + QColor outline = QColor(31, 31, 31); + if (opt->state & QStyle::State_Selected) outline = QColor(127, 127, 255); + + QLinearGradient fill(QPointF(0, 0), QPointF(0, r.height())); + //fill.setCoordinateMode(QLinearGradient::CoordinateMode::ObjectMode); + fill.setColorAt(0, QColor(95, 95, 95)); + fill.setColorAt(16.0/r.height(), QColor(63, 63, 63)); + fill.setColorAt(1.0 - (1.0 - 16.0/r.height()) / 2, QColor(55, 55, 55)); + fill.setColorAt(1, QColor(35, 35, 35)); + + painter->setRenderHint(QPainter::RenderHint::Antialiasing); + painter->setBrush(QBrush(fill)); + painter->setPen(QPen(QBrush(outline), 2)); + painter->drawRoundedRect(r, 8, 8); + + QRectF tr = r - QMargins(3, 2, 3, 0); + if (!node->name.empty()) { + painter->setPen(QColor(222, 222, 222)); + painter->drawText(tr, Qt::AlignLeft, QString::fromStdString(node->name)); + tr -= QMarginsF(0, painter->fontMetrics().height(), 0, 0); + } + painter->setPen(QColor(171, 171, 171)); + painter->drawText(tr, Qt::AlignLeft, QString::fromStdString(node->pluginName())); +} + +QRectF NodeObject::boundingRect() const { + return QRectF(0, 0, 192, 48);// + QMarginsF(8, 8, 8, 8); +} + +PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) { + this->in = in; + this->out = out; + + in->connections[out] = this; + out->connections[in] = this; + + QTimer::singleShot(1, [this] { this->in->scene()->addItem(this); }); + setZValue(-100); + setAcceptHoverEvents(true); + //setFlag(QGraphicsItem::GraphicsItemFlag::) + + QTimer::singleShot(1, [this] { + auto* op = static_cast(this->out->parentItem()->parentItem()); + auto* ip = static_cast(this->in->parentItem()->parentItem()); + connect(op, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds); + connect(op, &QGraphicsObject::yChanged, this, &PortConnectionObject::updateEnds); + connect(ip, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds); + connect(ip, &QGraphicsObject::xChanged, this, &PortConnectionObject::updateEnds); + + updateEnds(); + }); +} + +void PortConnectionObject::updateEnds() { + setPos((out->scenePos() + in->scenePos()) * .5); + update(); +} + +PortConnectionObject::~PortConnectionObject() { + in->connections.erase(out); + out->connections.erase(in); +} + +void PortConnectionObject::disconnect() { + out->port->disconnect(in->port); + delete this; +} + +void PortConnectionObject::hoverEnterEvent(QGraphicsSceneHoverEvent*) { + highlighted = true; + update(); +} + +void PortConnectionObject::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { + highlighted = false; + update(); +} + +void PortConnectionObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) { + auto* m = new QMenu(); + m->addAction("Disconnect", this, [this] { + disconnect(); + }); + m->popup(e->screenPos()); +} + +void PortConnectionObject::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) { + QColor c = tcolor[in->port->dataType()]; + if (highlighted) c = c.lighter(150); + + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(c)); + painter->drawPath(shape(2.5)); +} + +QRectF PortConnectionObject::boundingRect() const { + return shape().boundingRect().normalized();//controlPointRect().normalized().united(QRectF(mapFromScene(out->scenePos()), mapFromScene(in->scenePos()))); +} + +QPainterPath PortConnectionObject::shape(qreal width) const { + QPainterPath path; + auto start = mapFromScene(out->scenePos()); + auto end = mapFromScene(in->scenePos()); + path.moveTo(start); + //QPointF mod(std::max(std::max((end.x() - start.x()) * .64, (start.x() - end.x()) * .24), 64.0), 0); + QPointF mod(std::max((end.x() - start.x()) * .64, 96.0), 0); + path.cubicTo(start + mod, end - mod, end); + + if (width <= 0) return path; + QPainterPathStroker qp; + qp.setWidth(width); + qp.setCapStyle(Qt::PenCapStyle::RoundCap); + qp.setJoinStyle(Qt::PenJoinStyle::RoundJoin); + auto p = qp.createStroke(path); + return p; +} diff --git a/xybrid/ui/patchboard/nodeobject.h b/xybrid/ui/patchboard/nodeobject.h new file mode 100644 index 0000000..0c7c78d --- /dev/null +++ b/xybrid/ui/patchboard/nodeobject.h @@ -0,0 +1,105 @@ +#pragma once + +#include + +#include + +#include + +namespace Xybrid::UI { + class PortConnectionObject; + class PortObject : public QGraphicsObject { + friend class PortConnectionObject; + + std::shared_ptr port; + QGraphicsSimpleTextItem* labelShadow; + QGraphicsSimpleTextItem* label; + bool highlighted = false; + std::unique_ptr dragLine; + + std::unordered_map connections; + + void connectTo(PortObject*); + void setHighlighted(bool, bool hideLabel = false); + + protected: + + public: + enum { Type = UserType + 101 }; + int type() const override { return Type; } + + PortObject(const std::shared_ptr&); + ~PortObject() override; + + inline const std::shared_ptr& getPort() const { return port; } + + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; + + void hoverEnterEvent(QGraphicsSceneHoverEvent*) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override; + + void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override; + QRectF boundingRect() const override; + }; + + class PortConnectionObject : public QGraphicsObject { + friend class PortObject; + + PortObject* in; + PortObject* out; + bool highlighted = false; + + PortConnectionObject(PortObject* in, PortObject* out); + + void updateEnds(); + public: + ~PortConnectionObject() override; + + void disconnect(); + + void hoverEnterEvent(QGraphicsSceneHoverEvent*) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override; + + void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override; + QRectF boundingRect() const override; + QPainterPath shape(qreal width) const; + QPainterPath shape() const override { return shape(8); } + }; + + class NodeObject : public QGraphicsObject { + friend class PortObject; + + std::shared_ptr node; + std::unique_ptr inputPortContainer = nullptr; + std::unique_ptr outputPortContainer = nullptr; + + void onMoved(); + void bringToTop(bool force = false); + + void createPorts(); + void updateGeometry(); + protected: + + void focusInEvent(QFocusEvent*) override; + + public: + enum { Type = UserType + 100 }; + int type() const override { return Type; } + + NodeObject(const std::shared_ptr&); + + inline const std::shared_ptr& getNode() const { return node; } + void promptDelete(); + + void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override; + + void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override; + QRectF boundingRect() const override; + }; +} diff --git a/xybrid/ui/patchboard/patchboardscene.cpp b/xybrid/ui/patchboard/patchboardscene.cpp new file mode 100644 index 0000000..e42911d --- /dev/null +++ b/xybrid/ui/patchboard/patchboardscene.cpp @@ -0,0 +1,110 @@ +#include "patchboardscene.h" +using Xybrid::UI::PatchboardScene; + +#include + +#include +#include +#include +#include +#include + +#include + +#include "data/graph.h" +using namespace Xybrid::Data; +#include "config/pluginregistry.h" +using namespace Xybrid::Config; + +#include "ui/patchboard/nodeobject.h" + +#include "config/colorscheme.h" + +PatchboardScene::PatchboardScene(QGraphicsView* parent, const std::shared_ptr& g) : QGraphicsScene(parent) { + graph = g; + view = parent; + + connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::autoSetSize); + connect(view->verticalScrollBar(), &QScrollBar::valueChanged, this, &PatchboardScene::autoSetSize); + connect(view->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::autoSetSize); + connect(view->verticalScrollBar(), &QScrollBar::rangeChanged, this, &PatchboardScene::autoSetSize); + connect(this, &QGraphicsScene::changed, this, &PatchboardScene::autoSetSize); + + /*{ + auto* t = addEllipse(0, 0, 64, 32);//scene->addText("hello world"); + t->setBrush(QBrush(QColor(127,0,255))); + t->setFlag(QGraphicsItem::ItemIsMovable); + t->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + t->setFlag(QGraphicsItem::ItemIsSelectable); + } + + { + auto* t = addText("Hi there!"); + //t->setBrush(QBrush(QColor(191,127,255))); + t->setFlag(QGraphicsItem::ItemIsMovable); + t->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + t->setFlag(QGraphicsItem::ItemIsSelectable); + t->stackBefore(nullptr); + }//*/ + + refresh(); +} + +void PatchboardScene::drawBackground(QPainter* painter, const QRectF& rect) { + painter->setBrush(QBrush(Config::ColorScheme::current.patternBg)); + painter->setPen(QPen(Qt::PenStyle::NoPen)); + painter->drawRect(rect); + + const constexpr int step = 32; // grid size + painter->setPen(QPen(QColor(127, 127, 127, 63), 1)); + for (auto y = std::floor(rect.top() / step) * step + .5; y < rect.bottom(); y += step) + painter->drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)); + for (auto x = std::floor(rect.left() / step) * step + .5; x < rect.right(); x += step) + painter->drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom())); +} + +void PatchboardScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) { + // only override if nothing inside picks it up + QGraphicsScene::contextMenuEvent(e); + if (e->isAccepted()) return; + + auto p = e->scenePos(); + auto* m = new QMenu(); + PluginRegistry::populatePluginMenu(m->addMenu("Add..."), [this, p](std::shared_ptr n) { + n->x = static_cast(p.x()); + n->y = static_cast(p.y()); + n->parentTo(graph); + + addItem(new NodeObject(n)); + }); + + m->popup(e->screenPos()); + + /*qDebug() << "context menu requested for scene at" << p; + auto n = std::make_shared(); + n->x = static_cast(p.x()); + n->y = static_cast(p.y()); + n->parentTo(graph); + + addItem(new NodeObject(n)); + + e->accept();*/ +} + +void PatchboardScene::autoSetSize() { + auto rect = itemsBoundingRect() + .united(view->mapToScene(view->viewport()->visibleRegion().boundingRect()).boundingRect()) + .united(QRectF(0, 0, 1, 1)); + rect.setTopLeft(QPointF(0, 0)); + setSceneRect(rect); +} + +void PatchboardScene::refresh() { + // build scene from graph + clear(); + + for (auto n : graph->children) { + auto* o = new NodeObject(n); + addItem(o); + } +} diff --git a/xybrid/ui/patchboard/patchboardscene.h b/xybrid/ui/patchboard/patchboardscene.h new file mode 100644 index 0000000..1158130 --- /dev/null +++ b/xybrid/ui/patchboard/patchboardscene.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include + +namespace Xybrid::Data { class Graph; } + +namespace Xybrid::UI { + class PatchboardScene : public QGraphicsScene { + std::shared_ptr graph; + QGraphicsView* view; + + void autoSetSize(); + + public: + PatchboardScene(QGraphicsView* view, const std::shared_ptr& graph); + ~PatchboardScene() override = default; + + void drawBackground(QPainter*, const QRectF&) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override; + + void refresh(); + }; +} diff --git a/xybrid/ui/patterneditoritemdelegate.cpp b/xybrid/ui/patterneditoritemdelegate.cpp index 96b9da4..3daafb7 100644 --- a/xybrid/ui/patterneditoritemdelegate.cpp +++ b/xybrid/ui/patterneditoritemdelegate.cpp @@ -52,7 +52,7 @@ namespace { template [[maybe_unused]] void insertDigit(T& val, size_t hex) { // insert hex digit into a particular value - if (static_cast(val) == -1) val = 0; + if (static_cast(val) < 0) val = 0; val = static_cast((static_cast(val) & 15) * 16 + (hex & 15)); } @@ -220,6 +220,10 @@ bool PatternEditorItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *m row.port = -1; return dc->commit(); } + if (k == Qt::Key_G) { // global commands (tempo and the like) + row.port = -2; + return dc->commit(); + } for (size_t i = 0; i < 16; i++) { if (k == numberKeys[i]) { insertDigit(row.port, i); diff --git a/xybrid/ui/patterneditormodel.cpp b/xybrid/ui/patterneditormodel.cpp index 9c014b4..d006b7b 100644 --- a/xybrid/ui/patterneditormodel.cpp +++ b/xybrid/ui/patterneditormodel.cpp @@ -82,6 +82,7 @@ QVariant PatternEditorModel::data(const QModelIndex &index, int role) const { auto& row = pattern->rowAt(ch, index.row()); if (cc == 0) { // port if (row.port >= 0 && row.port < 256) return QString::fromStdString(byteStr(row.port)); + if (row.port == -2) return QString("(G)"); return QString(" - "); } else if (cc == 1) { // note if (row.note >= 0) return QString::fromStdString(noteStr(row.note)); diff --git a/xybrid/util/lambdaeventfilter.h b/xybrid/util/lambdaeventfilter.h new file mode 100644 index 0000000..ede450f --- /dev/null +++ b/xybrid/util/lambdaeventfilter.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +class LambdaEventFilter : public QObject { + Q_OBJECT + std::function filter; +public: + LambdaEventFilter(QObject* parent, std::function f) : QObject(parent), filter(f) { } + bool eventFilter(QObject* watched, QEvent* event) override { return filter(watched, event); } +}; diff --git a/xybrid/xybrid.pro b/xybrid/xybrid.pro index 2d90dd9..e867923 100644 --- a/xybrid/xybrid.pro +++ b/xybrid/xybrid.pro @@ -46,7 +46,11 @@ SOURCES += \ editing/projectcommands.cpp \ editing/compositecommand.cpp \ data/node.cpp \ - audio/audioengine.cpp + audio/audioengine.cpp \ + ui/patchboard/patchboardscene.cpp \ + ui/patchboard/nodeobject.cpp \ + data/graph.cpp \ + config/pluginregistry.cpp HEADERS += \ mainwindow.h \ @@ -67,7 +71,12 @@ HEADERS += \ editing/compositecommand.h \ data/node.h \ data/graph.h \ - audio/audioengine.h + audio/audioengine.h \ + ui/patchboard/patchboardscene.h \ + util/lambdaeventfilter.h \ + ui/patchboard/nodeobject.h \ + data/porttypes.h \ + config/pluginregistry.h FORMS += \ mainwindow.ui