xybrid/xybrid/ui/patchboard/nodeobject.cpp

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