diff --git a/CMakeLists.txt b/CMakeLists.txt index f383700..497e248 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ if(WARPPIPE_BUILD_GUI) gui/PresetManager.cpp gui/VolumeWidgets.cpp gui/AudioLevelMeter.cpp + gui/SquareConnectionPainter.cpp ) target_link_libraries(warppipe-gui PRIVATE @@ -102,6 +103,7 @@ if(WARPPIPE_BUILD_GUI) gui/PresetManager.cpp gui/VolumeWidgets.cpp gui/AudioLevelMeter.cpp + gui/SquareConnectionPainter.cpp ) target_compile_definitions(warppipe-gui-tests PRIVATE WARPPIPE_TESTING) diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index bb9061b..8dc75df 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -1,12 +1,14 @@ #include "AudioLevelMeter.h" #include "GraphEditorWidget.h" #include "PresetManager.h" +#include "SquareConnectionPainter.h" #include "VolumeWidgets.h" #include "WarpGraphModel.h" #include #include #include +#include #include #include #include @@ -641,6 +643,15 @@ void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos, autoArrange->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L)); QAction *refreshGraph = menu.addAction(QStringLiteral("Refresh Graph")); refreshGraph->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R)); + menu.addSeparator(); + auto *connStyleMenu = menu.addMenu(QStringLiteral("Connection Style")); + auto *styleBezier = connStyleMenu->addAction(QStringLiteral("Bezier Curves")); + styleBezier->setCheckable(true); + styleBezier->setChecked(m_connectionStyle == ConnectionStyleType::kBezier); + auto *styleSquare = connStyleMenu->addAction(QStringLiteral("Square Routing")); + styleSquare->setCheckable(true); + styleSquare->setChecked(m_connectionStyle == ConnectionStyleType::kSquare); + menu.addSeparator(); QAction *saveLayoutAs = menu.addAction(QStringLiteral("Save Layout As...")); QAction *resetLayout = menu.addAction(QStringLiteral("Reset Layout")); @@ -683,6 +694,10 @@ void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos, m_model->autoArrange(); m_view->zoomFitAll(); saveLayoutWithViewState(); + } else if (chosen == styleBezier) { + setConnectionStyle(ConnectionStyleType::kBezier); + } else if (chosen == styleSquare) { + setConnectionStyle(ConnectionStyleType::kSquare); } else if (chosen == savePresetAction) { savePreset(); } else if (chosen == loadPresetAction) { @@ -1103,6 +1118,7 @@ void GraphEditorWidget::saveLayoutWithViewState() { QList sizes = m_splitter->sizes(); vs.splitterGraph = sizes.value(0, 1200); vs.splitterSidebar = sizes.value(1, 320); + vs.connectionStyle = static_cast(m_connectionStyle); vs.valid = true; m_model->saveLayout(m_layoutPath, vs); } @@ -1115,6 +1131,9 @@ void GraphEditorWidget::restoreViewState() { if (vs.splitterGraph > 0 || vs.splitterSidebar > 0) { m_splitter->setSizes({vs.splitterGraph, vs.splitterSidebar}); } + if (vs.connectionStyle == static_cast(ConnectionStyleType::kSquare)) { + setConnectionStyle(ConnectionStyleType::kSquare); + } } else { m_view->zoomFitAll(); } @@ -1540,6 +1559,25 @@ void GraphEditorWidget::rebuildRulesList() { static_cast(layout)->addStretch(); } +void GraphEditorWidget::setConnectionStyle(ConnectionStyleType style) { + if (style == m_connectionStyle) + return; + m_connectionStyle = style; + + if (style == ConnectionStyleType::kSquare) { + m_scene->setConnectionPainter(std::make_unique()); + } else { + m_scene->setConnectionPainter( + std::make_unique()); + } + + for (auto *item : m_scene->items()) { + item->update(); + } + + scheduleSaveLayout(); +} + void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp, const std::string &prefillBin, const std::string &prefillRole, diff --git a/gui/GraphEditorWidget.h b/gui/GraphEditorWidget.h index a0bd9fd..c56998f 100644 --- a/gui/GraphEditorWidget.h +++ b/gui/GraphEditorWidget.h @@ -27,6 +27,11 @@ class QTabWidget; class QTimer; class DeleteVirtualNodeCommand; +enum class ConnectionStyleType : uint8_t { + kBezier = 0, + kSquare, +}; + class GraphEditorWidget : public QWidget { Q_OBJECT @@ -79,6 +84,7 @@ private: const std::string &prefillRole = {}, const std::string &prefillTarget = {}, warppipe::RuleId editRuleId = {}); + void setConnectionStyle(ConnectionStyleType style); struct PendingPasteLink { std::string outNodeName; @@ -121,4 +127,6 @@ private: QWidget *m_rulesContainer = nullptr; QScrollArea *m_rulesScroll = nullptr; + + ConnectionStyleType m_connectionStyle = ConnectionStyleType::kBezier; }; diff --git a/gui/SquareConnectionPainter.cpp b/gui/SquareConnectionPainter.cpp new file mode 100644 index 0000000..752a7d6 --- /dev/null +++ b/gui/SquareConnectionPainter.cpp @@ -0,0 +1,116 @@ +#include "SquareConnectionPainter.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +QPainterPath SquareConnectionPainter::orthogonalPath( + QtNodes::ConnectionGraphicsObject const &cgo) const { + QPointF out = cgo.endPoint(QtNodes::PortType::Out); + QPointF in = cgo.endPoint(QtNodes::PortType::In); + + constexpr double kRadius = 5.0; + constexpr double kSpacing = 8.0; + constexpr double kMinStub = 20.0; + + auto cId = cgo.connectionId(); + double spread = static_cast(cId.outPortIndex) * kSpacing; + + double dy = in.y() - out.y(); + + if (std::abs(dy) < 0.5 && in.x() > out.x()) { + QPainterPath path(out); + path.lineTo(in); + return path; + } + + double midX = (out.x() + in.x()) / 2.0 + spread; + midX = std::max(midX, out.x() + kMinStub); + if (in.x() > out.x()) + midX = std::min(midX, in.x() - kMinStub); + + double r = std::min({kRadius, std::abs(dy) / 2.0, + std::abs(midX - out.x()), + std::abs(in.x() - midX)}); + r = std::max(r, 0.0); + + double sy = (dy > 0) ? 1.0 : -1.0; + double hDir = (in.x() >= midX) ? 1.0 : -1.0; + + QPainterPath path(out); + + if (r > 0.5) { + path.lineTo(midX - r, out.y()); + path.quadTo(QPointF(midX, out.y()), QPointF(midX, out.y() + sy * r)); + path.lineTo(midX, in.y() - sy * r); + path.quadTo(QPointF(midX, in.y()), QPointF(midX + hDir * r, in.y())); + } else { + path.lineTo(midX, out.y()); + path.lineTo(midX, in.y()); + } + + path.lineTo(in); + return path; +} + +void SquareConnectionPainter::paint( + QPainter *painter, + QtNodes::ConnectionGraphicsObject const &cgo) const { + auto const &style = QtNodes::StyleCollection::connectionStyle(); + + bool const hovered = cgo.connectionState().hovered(); + bool const selected = cgo.isSelected(); + bool const sketch = cgo.connectionState().requiresPort(); + + auto path = orthogonalPath(cgo); + + if (hovered || selected) { + QPen pen; + pen.setWidth(static_cast(2 * style.lineWidth())); + pen.setColor(selected ? style.selectedHaloColor() : style.hoveredColor()); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } + + if (sketch) { + QPen pen; + pen.setWidth(static_cast(style.constructionLineWidth())); + pen.setColor(style.constructionColor()); + pen.setStyle(Qt::DashLine); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } else { + QPen pen; + pen.setWidth(style.lineWidth()); + pen.setColor(selected ? style.selectedColor() : style.normalColor()); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path); + } + + double const pointRadius = style.pointDiameter() / 2.0; + painter->setPen(style.constructionColor()); + painter->setBrush(style.constructionColor()); + painter->drawEllipse(cgo.out(), pointRadius, pointRadius); + painter->drawEllipse(cgo.in(), pointRadius, pointRadius); +} + +QPainterPath SquareConnectionPainter::getPainterStroke( + QtNodes::ConnectionGraphicsObject const &cgo) const { + auto path = orthogonalPath(cgo); + + QPainterPathStroker stroker; + stroker.setWidth(10.0); + + return stroker.createStroke(path); +} diff --git a/gui/SquareConnectionPainter.h b/gui/SquareConnectionPainter.h new file mode 100644 index 0000000..8c6dc44 --- /dev/null +++ b/gui/SquareConnectionPainter.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class SquareConnectionPainter : public QtNodes::AbstractConnectionPainter { +public: + void paint(QPainter *painter, + QtNodes::ConnectionGraphicsObject const &cgo) const override; + QPainterPath + getPainterStroke(QtNodes::ConnectionGraphicsObject const &cgo) const override; + +private: + QPainterPath + orthogonalPath(QtNodes::ConnectionGraphicsObject const &cgo) const; +}; diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index 2dcb566..80d4dcd 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -905,6 +905,7 @@ void WarpGraphModel::saveLayout(const QString &path, viewObj["splitter_graph"] = viewState.splitterGraph; viewObj["splitter_sidebar"] = viewState.splitterSidebar; } + viewObj["connection_style"] = viewState.connectionStyle; root["view"] = viewObj; } @@ -964,6 +965,7 @@ bool WarpGraphModel::loadLayout(const QString &path) { m_savedViewState.centerY = viewObj["center_y"].toDouble(); m_savedViewState.splitterGraph = viewObj["splitter_graph"].toInt(0); m_savedViewState.splitterSidebar = viewObj["splitter_sidebar"].toInt(0); + m_savedViewState.connectionStyle = viewObj["connection_style"].toInt(0); m_savedViewState.valid = true; } diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index 373b62c..b339057 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -88,6 +88,7 @@ public: double centerY; int splitterGraph; int splitterSidebar; + int connectionStyle; bool valid; };