logo and (incomplete) working patchboard

portability/boost
zetaPRIME 2018-12-22 21:03:51 -05:00
parent 76568244ff
commit f78c3da03d
26 changed files with 1021 additions and 31 deletions

Binary file not shown.

BIN
asset-work/xybrid-logo.xcf Normal file

Binary file not shown.

18
notes
View File

@ -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<datatype> of map<uchar>s
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.)

View File

@ -105,7 +105,7 @@ void AudioEngine::play(std::shared_ptr<Project> 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<int>(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++;

View File

@ -70,7 +70,7 @@ namespace Xybrid::Audio {
volatile int note = 12*5;
signals:
void playbackModeChanged(PlaybackMode);
void playbackModeChanged();
public slots:
};

View File

@ -0,0 +1,59 @@
#include "pluginregistry.h"
using namespace Xybrid::Config;
#include <list>
#include <map>
#include <unordered_map>
#include <QMenu>
#include "data/node.h"
using namespace Xybrid::Data;
namespace {
typedef std::list<std::function<void()>> 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<std::string, std::shared_ptr<PluginInfo>> plugins;
}
bool PluginRegistry::enqueueRegistration(std::function<void ()> 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<PluginInfo> pi) {
if (pi->id.empty()) return;
if (plugins.find(pi->id) != plugins.end()) return;
plugins[pi->id] = pi;
}
std::shared_ptr<Node> 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<void (std::shared_ptr<Node>)> 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);
});
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <memory>
#include <functional>
class QMenu;
namespace Xybrid::Data {
class Node;
}
namespace Xybrid::Config {
class PluginInfo {
public:
std::string id;
std::string displayName;
std::string category;
std::function<std::shared_ptr<Data::Node>()> createInstance;
PluginInfo() = default;
virtual ~PluginInfo() = default;
};
namespace PluginRegistry {
bool enqueueRegistration(std::function<void()>);
void registerPlugin(std::shared_ptr<PluginInfo>);
void init();
std::shared_ptr<Data::Node> createInstance(const std::string& id);
void populatePluginMenu(QMenu*, std::function<void(std::shared_ptr<Data::Node>)>);
}
}

17
xybrid/data/graph.cpp Normal file
View File

@ -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<PluginInfo>();
i->id = "graph";
i->displayName = "Subgraph";
i->createInstance = []{ return std::make_shared<Graph>(); };
PluginRegistry::registerPlugin(i);
});
}
//std::string Graph::pluginName() const { return "Subgraph"; }

View File

@ -6,5 +6,10 @@ namespace Xybrid::Data {
class Graph : public Node {
public:
std::vector<std::shared_ptr<Node>> children;
// position of viewport within graph (not serialized)
int viewX{}, viewY{};
//std::string pluginName() const override;
};
}

View File

@ -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 <algorithm>
bool Port::canConnectTo(DataType d) {
#include <QDebug>
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<Port> 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<Port> 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> Port::makePort(DataType dt) {
if (dt == Audio) return std::make_shared<AudioPort>();
if (dt == Command) return std::make_shared<CommandPort>();
// fallback
return std::make_shared<Port>();
}
void Node::parentTo(std::shared_ptr<Graph> 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) {
graph->children.push_back(t);
}
}
std::shared_ptr<Port> 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<Port> 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; }

View File

@ -2,35 +2,54 @@
#include <memory>
#include <vector>
#include <map>
#include <string>
#include <QPointer>
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<Port> {
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<Node> owner;
std::vector<std::weak_ptr<Port>> connections;
std::weak_ptr<Port> passthroughFor;
Type type; // TODO: figure out passthrough?
uint8_t index;
size_t tickUpdatedOn = static_cast<size_t>(-1);
virtual ~Port() = default;
QPointer<UI::PortObject> 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<DataType>(-1); }
virtual bool singleInput() const { return false; }
virtual bool canConnectTo(DataType) const;
/*virtual*/ bool connect(std::shared_ptr<Port>);
/*virtual*/ void disconnect(std::shared_ptr<Port>);
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<Port> makePort(DataType);
};
class Node : public std::enable_shared_from_this<Node> {
@ -39,12 +58,19 @@ namespace Xybrid::Data {
int x{}, y{};
std::string name;
std::vector<std::shared_ptr<Port>> inputs, outputs;
std::map<Port::DataType, std::map<uint8_t, std::shared_ptr<Port>>> inputs, outputs;
std::shared_ptr<Config::PluginInfo> plugin;
virtual ~Node() = default;
void parentTo(std::shared_ptr<Graph>);
std::shared_ptr<Port> port(Port::Type, Port::DataType, uint8_t, bool addIfNeeded = false);
std::shared_ptr<Port> addPort(Port::Type, Port::DataType, uint8_t);
void removePort(Port::Type, Port::DataType, uint8_t);
virtual void process() { }
virtual std::string pluginName() const;
};
}

23
xybrid/data/porttypes.h Normal file
View File

@ -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; }
};
}

View File

@ -1,18 +1,26 @@
#include "mainwindow.h"
#include "audio/audioengine.h"
#include "config/pluginregistry.h"
#include <vector>
#include <QDebug>
#include <QFontDatabase>
#include <QApplication>
#include <QSurfaceFormat>
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();

View File

@ -11,27 +11,44 @@ using Xybrid::MainWindow;
#include <QMessageBox>
#include <QWindow>
#include <QUndoStack>
#include <QTimer>
#include <QOpenGLWidget>
#include <QScroller>
#include <QGraphicsTextItem>
#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<QMouseEvent*>(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>();
project->sequence.push_back(project->newPattern().get());
// TEMP add some stuff
{
auto g1 = PluginRegistry::createInstance("graph");//std::make_shared<Graph>();
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<Data::Graph>& g) {
if (!g) return; // invalid
ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g));
}

View File

@ -32,6 +32,8 @@ namespace Xybrid {
void updatePatternLists();
bool selectPatternForEditing(Data::Pattern*);
void openGraph(const std::shared_ptr<Data::Graph>&);
void updateTitle();
public:

View File

@ -33,7 +33,7 @@
<enum>Qt::NoFocus</enum>
</property>
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<property name="documentMode">
<bool>true</bool>
@ -249,7 +249,11 @@
<number>0</number>
</property>
<item>
<widget class="QGraphicsView" name="patchboardView"/>
<widget class="QGraphicsView" name="patchboardView">
<property name="viewportUpdateMode">
<enum>QGraphicsView::FullViewportUpdate</enum>
</property>
</widget>
</item>
</layout>
</widget>
@ -257,17 +261,20 @@
<attribute name="title">
<string>nonexistent</string>
</attribute>
<widget class="QLabel" name="label">
<widget class="QLabel" name="logo">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>63</width>
<height>16</height>
<width>81</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>(logo here)</string>
<string/>
</property>
<property name="pixmap">
<pixmap resource="res/resources.qrc">:/img/xybrid-logo-tiny.png</pixmap>
</property>
</widget>
</widget>
@ -366,7 +373,9 @@
<header>ui/patterneditorview.h</header>
</customwidget>
</customwidgets>
<resources/>
<resources>
<include location="res/resources.qrc"/>
</resources>
<connections>
<connection>
<sender>actionNew</sender>

View File

@ -2,4 +2,7 @@
<qresource prefix="/fonts">
<file>iosevka-term-light.ttf</file>
</qresource>
<qresource prefix="/img">
<file>xybrid-logo-tiny.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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 <cmath>
#include <QDebug>
#include <QTimer>
#include <QPainter>
#include <QGraphicsScene>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsSceneHoverEvent>
#include <QTextDocument>
#include <QTextCharFormat>
#include <QTextCursor>
#include <QMenu>
#include <QMessageBox>
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<Data::Port>& 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<PortObject*>(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<Data::Node>& 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<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 (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<int>(x());
node->y = static_cast<int>(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<QGraphicsObject*>(this->out->parentItem()->parentItem());
auto* ip = static_cast<QGraphicsObject*>(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;
}

View File

@ -0,0 +1,105 @@
#pragma once
#include <unordered_map>
#include <QGraphicsObject>
#include <data/node.h>
namespace Xybrid::UI {
class PortConnectionObject;
class PortObject : public QGraphicsObject {
friend class PortConnectionObject;
std::shared_ptr<Data::Port> port;
QGraphicsSimpleTextItem* labelShadow;
QGraphicsSimpleTextItem* label;
bool highlighted = false;
std::unique_ptr<QGraphicsLineItem> dragLine;
std::unordered_map<PortObject*, PortConnectionObject*> 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<Data::Port>&);
~PortObject() override;
inline const std::shared_ptr<Data::Port>& 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<Data::Node> node;
std::unique_ptr<QGraphicsItem> inputPortContainer = nullptr;
std::unique_ptr<QGraphicsItem> 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<Data::Node>&);
inline const std::shared_ptr<Data::Node>& getNode() const { return node; }
void promptDelete();
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) override;
QRectF boundingRect() const override;
};
}

View File

@ -0,0 +1,110 @@
#include "patchboardscene.h"
using Xybrid::UI::PatchboardScene;
#include <cmath>
#include <QDebug>
#include <QScrollBar>
#include <QGraphicsItem>
#include <QMainWindow>
#include <QMenu>
#include <QGraphicsSceneContextMenuEvent>
#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<Xybrid::Data::Graph>& 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<Node> n) {
n->x = static_cast<int>(p.x());
n->y = static_cast<int>(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<Data::Node>();
n->x = static_cast<int>(p.x());
n->y = static_cast<int>(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);
}
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <memory>
#include <QGraphicsScene>
#include <QGraphicsView>
namespace Xybrid::Data { class Graph; }
namespace Xybrid::UI {
class PatchboardScene : public QGraphicsScene {
std::shared_ptr<Data::Graph> graph;
QGraphicsView* view;
void autoSetSize();
public:
PatchboardScene(QGraphicsView* view, const std::shared_ptr<Data::Graph>& graph);
~PatchboardScene() override = default;
void drawBackground(QPainter*, const QRectF&) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
void refresh();
};
}

View File

@ -52,7 +52,7 @@ namespace {
template <typename T>
[[maybe_unused]] void insertDigit(T& val, size_t hex) { // insert hex digit into a particular value
if (static_cast<int>(val) == -1) val = 0;
if (static_cast<int>(val) < 0) val = 0;
val = static_cast<T>((static_cast<size_t>(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);

View File

@ -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));

View File

@ -0,0 +1,12 @@
#pragma once
#include <functional>
#include <QObject>
class LambdaEventFilter : public QObject {
Q_OBJECT
std::function<bool(QObject*, QEvent*)> filter;
public:
LambdaEventFilter(QObject* parent, std::function<bool(QObject*, QEvent*)> f) : QObject(parent), filter(f) { }
bool eventFilter(QObject* watched, QEvent* event) override { return filter(watched, event); }
};

View File

@ -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