logo and (incomplete) working patchboard
parent
76568244ff
commit
f78c3da03d
Binary file not shown.
Binary file not shown.
18
notes
18
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<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.)
|
||||
|
|
|
@ -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++;
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace Xybrid::Audio {
|
|||
volatile int note = 12*5;
|
||||
|
||||
signals:
|
||||
void playbackModeChanged(PlaybackMode);
|
||||
void playbackModeChanged();
|
||||
|
||||
public slots:
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
}
|
|
@ -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>)>);
|
||||
}
|
||||
}
|
|
@ -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"; }
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ namespace Xybrid {
|
|||
void updatePatternLists();
|
||||
bool selectPatternForEditing(Data::Pattern*);
|
||||
|
||||
void openGraph(const std::shared_ptr<Data::Graph>&);
|
||||
|
||||
void updateTitle();
|
||||
|
||||
public:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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); }
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue