501 lines
16 KiB
C++
501 lines
16 KiB
C++
#include "nodeobject.h"
|
|
using Xybrid::UI::NodeObject;
|
|
using Xybrid::UI::PortObject;
|
|
using Xybrid::UI::PortConnectionObject;
|
|
using Xybrid::Data::Node;
|
|
using Xybrid::Data::Port;
|
|
|
|
#include "fileops.h"
|
|
|
|
#include "ui/patchboard/gadgetscene.h"
|
|
|
|
#include "util/strings.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include <QDebug>
|
|
#include <QTimer>
|
|
#include <QPainter>
|
|
#include <QGraphicsScene>
|
|
#include <QStyleOptionGraphicsItem>
|
|
#include <QGraphicsSceneHoverEvent>
|
|
#include <QTextDocument>
|
|
#include <QTextCharFormat>
|
|
#include <QTextCursor>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QInputDialog>
|
|
|
|
|
|
namespace {
|
|
const QColor tcolor[] {
|
|
QColor(239, 179, 59), // Audio
|
|
QColor(163, 95, 191), // Command
|
|
QColor(95, 191, 163), // MIDI
|
|
QColor(127, 127, 255), // Parameter
|
|
};
|
|
|
|
const constexpr qreal edgePad = 3;
|
|
}
|
|
|
|
void PortObject::connectTo(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; }
|
|
|
|
// splice-between logic
|
|
auto dt = port->dataType();
|
|
if (in->port->type == Port::Input && in->port->singleInput() && in->port->isConnected() && out->port->dataType() == dt) {
|
|
if (auto oi = out->port->owner.lock()->port(Port::Input, dt, 0); oi) {
|
|
auto c = in->port->connections[0].lock();
|
|
if (!oi->isConnected() || oi->connections[0].lock() == in->port->connections[0].lock()) {
|
|
if (auto pc = in->connections[c->obj]; pc) delete pc;
|
|
in->port->disconnect(c);
|
|
|
|
if (!oi->isConnected() && oi->connect(c)) new PortConnectionObject(oi->obj, c->obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (out->port->connect(in->port)) {
|
|
/*auto* pc =*/ new PortConnectionObject(in, out);
|
|
}
|
|
|
|
}
|
|
|
|
void PortObject::setHighlighted(bool h, bool hideLabel) {
|
|
highlighted = h;
|
|
|
|
auto gs = static_cast<GadgetScene*>(scene());
|
|
if (h && !hideLabel) {
|
|
auto c = tcolor[port->dataType()];
|
|
auto txt = qs("%1 %2").arg(Util::enumName(port->dataType()).toLower(), Util::hex(port->index));
|
|
if (!port->name.isEmpty()) txt = qs("%1 (%2)").arg(port->name, txt);
|
|
double side = port->type == Port::Input ? -1.0 : 1.0;
|
|
gs->toolTip(this, txt, {side, 0}, c);
|
|
} else gs->toolTip(this);
|
|
|
|
update();
|
|
}
|
|
|
|
PortObject::PortObject(const std::shared_ptr<Data::Port>& p) {
|
|
port = p;
|
|
p->obj = this;
|
|
setAcceptHoverEvents(true);
|
|
setAcceptedMouseButtons(Qt::LeftButton);
|
|
setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
|
|
|
|
for (auto& c : port->connections) {
|
|
if (auto cc = c.lock(); cc && cc->obj) connectTo(cc->obj);
|
|
}
|
|
|
|
setCursor(Qt::CursorShape::CrossCursor);
|
|
}
|
|
|
|
PortObject::~PortObject() {
|
|
while (connections.begin() != connections.end()) delete connections.begin()->second;
|
|
}
|
|
|
|
void PortObject::mousePressEvent(QGraphicsSceneMouseEvent*) {
|
|
//setCursor(Qt::CursorShape::CrossCursor);
|
|
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->setAttribute(Qt::WA_DeleteOnClose);
|
|
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;
|
|
node->obj = this;
|
|
|
|
setFlag(QGraphicsItem::ItemIsMovable);
|
|
setFlag(QGraphicsItem::ItemIsFocusable);
|
|
setFlag(QGraphicsItem::ItemIsSelectable);
|
|
setFlag(QGraphicsItem::ItemSendsScenePositionChanges);
|
|
|
|
setPos(node->x, node->y);
|
|
|
|
connect(this, &QGraphicsObject::xChanged, this, &NodeObject::onMoved);
|
|
connect(this, &QGraphicsObject::yChanged, this, &NodeObject::onMoved);
|
|
|
|
//setToolTip(QString::fromStdString(node->name));
|
|
|
|
contents = new QGraphicsRectItem(this);
|
|
contents->setPen(Qt::NoPen);
|
|
contents->setBrush(Qt::NoBrush);
|
|
|
|
createPorts();
|
|
|
|
node->onGadgetCreated();
|
|
|
|
emit finalized(); // clazy:exclude=incorrect-emit
|
|
}
|
|
|
|
void NodeObject::setGadgetSize(QPointF p) {
|
|
gadgetSize_ = p;
|
|
updateGeometry();
|
|
}
|
|
|
|
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::Yes) != QMessageBox::Yes) return;
|
|
if (t) {
|
|
t->node->parentTo(nullptr); // unparent node so it gets deleted
|
|
delete t;
|
|
}
|
|
}
|
|
|
|
void NodeObject::mouseMoveEvent(QGraphicsSceneMouseEvent* e) {
|
|
QGraphicsObject::mouseMoveEvent(e);
|
|
if (e->modifiers() & Qt::Modifier::SHIFT) {
|
|
auto oPos = pos();
|
|
// snap to grid (by center) if shift held
|
|
constexpr qreal grid = 16.0;
|
|
auto r = boundingRect();
|
|
auto cx = r.width()/2.0;
|
|
auto cy = r.height()/2.0;
|
|
setPos(std::round((x()+cx)/grid)*grid-cx, std::round((y()+cy)/grid)*grid-cy);
|
|
|
|
auto posDelta = pos() - oPos;
|
|
for (auto itm : scene()->selectedItems()) { // clazy:exclude=range-loop-detach
|
|
if (itm == this) continue;
|
|
itm->moveBy(posDelta.x(), posDelta.y()); // apply snap to everything else, in relative terms
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
|
|
node->onDoubleClick();
|
|
}
|
|
|
|
void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
|
|
if (!isSelected()) {
|
|
for (auto* s : scene()->selectedItems()) s->setSelected(false); // clazy:exclude=range-loop-detach
|
|
setSelected(true);
|
|
}
|
|
auto* m = new QMenu();
|
|
if (canRename) {
|
|
m->addAction("Rename...", this, [this] {
|
|
bool ok = false;
|
|
auto cn = node->name.isEmpty() ? node->pluginName() : QString("\"%1\"").arg(node->name);
|
|
auto capt = QString("Rename %1:").arg(cn);
|
|
auto n = QInputDialog::getText(nullptr, "Rename...", capt, QLineEdit::Normal, node->name, &ok);
|
|
if (!ok) return; // canceled
|
|
//setToolTip(n);
|
|
node->name = n;
|
|
node->onRename();
|
|
updateGeometry();
|
|
update();
|
|
});
|
|
}
|
|
m->addAction("Export...", this, [this] {
|
|
if (auto fileName = FileOps::showSaveAsDialog(nullptr, "Export node...", Config::Directories::presets, FileOps::Filter::node, "xyn"); !fileName.isEmpty()) {
|
|
FileOps::saveNode(node, fileName);
|
|
}
|
|
});
|
|
m->addSeparator();
|
|
m->addAction("Delete node", this, &NodeObject::promptDelete);
|
|
m->setAttribute(Qt::WA_DeleteOnClose);
|
|
m->popup(e->screenPos());
|
|
}
|
|
|
|
void NodeObject::bringToTop(bool force) {
|
|
for (auto o : collidingItems()) if (o->parentItem() == parentItem() && (force || !o->isSelected())) o->stackBefore(this); // clazy:exclude=range-loop-detach
|
|
}
|
|
|
|
void NodeObject::createPorts() {
|
|
auto* ipc = new QGraphicsLineItem(this);
|
|
auto* opc = new QGraphicsLineItem(this);
|
|
inputPortContainer.reset(ipc);
|
|
outputPortContainer.reset(opc);
|
|
updateGeometry();
|
|
|
|
QPen p(QColor(95, 95, 95), 2.5);
|
|
QPointF inc(0, PortObject::portSize + PortObject::portSpacing);
|
|
|
|
QPointF cursor = QPointF(0, 0);
|
|
for (auto& mdt : node->inputs) {
|
|
for (auto& pp : mdt.second) {
|
|
auto* p = new PortObject(pp.second);
|
|
p->setParentItem(ipc);
|
|
p->setPos(cursor);
|
|
cursor += inc;
|
|
}
|
|
}
|
|
ipc->setVisible(cursor.y() > 0);
|
|
cursor -= inc;
|
|
ipc->setLine(QLineF(QPointF(0, 0), cursor));
|
|
ipc->setPen(p);
|
|
|
|
cursor = QPointF(0, 0);
|
|
for (auto& mdt : node->outputs) {
|
|
for (auto& pp : mdt.second) {
|
|
auto* p = new PortObject(pp.second);
|
|
p->setParentItem(opc);
|
|
p->setPos(cursor);
|
|
cursor += inc;
|
|
}
|
|
}
|
|
opc->setVisible(cursor.y() > 0);
|
|
cursor -= inc;
|
|
opc->setLine(QLineF(QPointF(0, 0), cursor));
|
|
opc->setPen(p);
|
|
}
|
|
|
|
void NodeObject::updateGeometry() {
|
|
contents->setRect(QRectF(QPointF(0, 0), gadgetSize_));
|
|
if (showName) contents->setPos(edgePad, edgePad + nameSize());
|
|
else contents->setPos(edgePad, edgePad);
|
|
|
|
if (autoPositionPorts) {
|
|
qreal pm = PortObject::portSize * .5 + PortObject::portSpacing;
|
|
if (inputPortContainer) inputPortContainer->setPos(QPointF(-pm, PortObject::portSize));
|
|
if (outputPortContainer) outputPortContainer->setPos(QPointF(boundingRect().width() + pm, PortObject::portSize)); // NOLINT we're calling this on *this*
|
|
}
|
|
emit postGeometryUpdate();
|
|
}
|
|
|
|
qreal NodeObject::nameSize() const {
|
|
qreal n = 0;
|
|
if (showName) {
|
|
if (showPluginName) n++;
|
|
if (!node->name.isEmpty()) n++;
|
|
}
|
|
if (n > 0) {
|
|
n *= QFontMetrics(QFont()).height();
|
|
n--; // decreased top padding
|
|
}
|
|
return n;
|
|
}
|
|
|
|
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::drawPanel(QPainter* painter, const QStyleOptionGraphicsItem* opt, QRectF r, double rad) {
|
|
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.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, rad, rad);
|
|
}
|
|
|
|
void NodeObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt, QWidget *) {
|
|
if (customChrome) {
|
|
node->drawCustomChrome(painter, opt);
|
|
return;
|
|
}
|
|
QRectF r = boundingRect();
|
|
|
|
drawPanel(painter, opt, r);
|
|
|
|
if (showName) {
|
|
static QFont f = [] {
|
|
QFont f("Arcon Rounded", 8);
|
|
f.setStretch(QFont::SemiCondensed);
|
|
return f;
|
|
}();
|
|
|
|
painter->setFont(f);
|
|
QRectF tr = r - QMarginsF(edgePad, edgePad - 1, edgePad, 0);
|
|
if (!node->name.isEmpty()) {
|
|
painter->setPen(QColor(222, 222, 222));
|
|
painter->drawText(tr, Qt::AlignLeft, node->name);
|
|
tr -= QMarginsF(0, painter->fontMetrics().height(), 0, 0);
|
|
}
|
|
if (showPluginName) {
|
|
painter->setPen(QColor(171, 171, 171));
|
|
painter->drawText(tr, Qt::AlignLeft, node->pluginName());
|
|
}
|
|
}
|
|
}
|
|
|
|
QRectF NodeObject::boundingRect() const {
|
|
if (customChrome) return QRectF(QPointF(0, 0), gadgetSize_);
|
|
if (gadgetSize_.isNull()) return QRectF(0, 0, 128+32, 36);// + QMarginsF(8, 8, 8, 8);
|
|
if (showName) return QRectF(QPointF(), gadgetSize_ + QPointF(edgePad * 2, edgePad * 2 + nameSize()));
|
|
return QRectF(QPointF(), gadgetSize_ + QPointF(edgePad * 2, edgePad * 2));
|
|
}
|
|
|
|
PortConnectionObject::PortConnectionObject(PortObject* in, PortObject* out) {
|
|
this->in = in;
|
|
this->out = out;
|
|
|
|
// remove dupes
|
|
if (in->connections[out]) delete in->connections[out];
|
|
if (out->connections[in]) delete out->connections[in];
|
|
// and hook up
|
|
in->connections[out] = this;
|
|
out->connections[in] = this;
|
|
|
|
QTimer::singleShot(1, this, [this] { this->in->scene()->addItem(this); });
|
|
setZValue(-100);
|
|
setAcceptHoverEvents(true);
|
|
|
|
QTimer::singleShot(1, this, [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 out(0, 0);
|
|
//path.lineTo(start+out);
|
|
QPointF mod(std::max((end.x() - start.x()) * .64, 96.0), 0);
|
|
if (auto vdist = std::fabs(end.y() - start.y()), hdist = fabs(end.x() - start.x()) * 0.75; hdist < 96.0 && vdist < 96.0) {
|
|
double p = std::max(vdist, hdist) / 96.0;
|
|
mod = QPointF(mod.x() * p, 0);
|
|
}
|
|
path.cubicTo(start + out + mod, end - mod, end - out);
|
|
//path.lineTo(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;
|
|
}
|