xybrid/xybrid/ui/patchboard/nodeobject.cpp

424 lines
13 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 <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>
#include "util/strings.h"
namespace {
const QColor tcolor[] {
QColor(239, 179, 59), // Audio
QColor(163, 95, 191), // Command
QColor(95, 191, 163), // MIDI
QColor(127, 127, 255), // Parameter
};
}
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; }
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(Util::enumName(port->dataType()).toLower()).arg(Util::hex(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);
}
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));
createPorts();
node->onGadgetCreated();
}
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::No) != QMessageBox::Yes) return;
if (t) {
t->node->parentTo(nullptr); // unparent node so it gets deleted
delete t;
}
}
void NodeObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) {
node->onDoubleClick();
}
void NodeObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* e) {
if (!isSelected()) {
for (auto* s : scene()->selectedItems()) s->setSelected(false);
setSelected(true);
}
auto* m = new QMenu();
if (canRename) {
m->addAction("Rename...", this, [this] {
bool ok = false;
auto cn = node->name.empty() ? QString::fromStdString(node->pluginName()) : QString("\"%1\"").arg(QString::fromStdString(node->name));
auto capt = QString("Rename %1:").arg(cn);
auto n = QInputDialog::getText(nullptr, "Rename...", capt, QLineEdit::Normal, QString::fromStdString(node->name), &ok);
if (!ok) return; // canceled
//setToolTip(n);
node->name = n.toStdString();
node->onRename();
});
}
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);
}
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() {
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));
}
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 *) {
if (customChrome) {
node->drawCustomChrome(painter, opt);
return;
}
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.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 {
if (customChrome) return QRectF(QPointF(0, 0), gadgetSize_);
return QRectF(0, 0, 192, 36);// + QMarginsF(8, 8, 8, 8);
}
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->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 out(0, 0);
//path.lineTo(start+out);
QPointF mod(std::max((end.x() - start.x()) * .64, 96.0), 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;
}