Fix perf issues and some visual bugs

This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 07:31:20 -07:00
commit d389161f4a
5 changed files with 300 additions and 11 deletions

View file

@ -4,6 +4,7 @@
#include "SquareConnectionPainter.h" #include "SquareConnectionPainter.h"
#include "VolumeWidgets.h" #include "VolumeWidgets.h"
#include "WarpGraphModel.h" #include "WarpGraphModel.h"
#include "ZoomGraphicsView.h"
#include <QtNodes/BasicGraphicsScene> #include <QtNodes/BasicGraphicsScene>
#include <QtNodes/ConnectionStyle> #include <QtNodes/ConnectionStyle>
@ -178,6 +179,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
bool hasLayout = m_model->loadLayout(m_layoutPath); bool hasLayout = m_model->loadLayout(m_layoutPath);
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
QtNodes::ConnectionStyle::setConnectionStyle( QtNodes::ConnectionStyle::setConnectionStyle(
R"({"ConnectionStyle": { R"({"ConnectionStyle": {
@ -192,10 +194,12 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
"UseDataDefinedColors": false "UseDataDefinedColors": false
}})"); }})");
m_view = new QtNodes::GraphicsView(m_scene); m_view = new ZoomGraphicsView(m_scene);
m_view->setFocusPolicy(Qt::StrongFocus); m_view->setFocusPolicy(Qt::StrongFocus);
m_view->viewport()->setFocusPolicy(Qt::StrongFocus); m_view->viewport()->setFocusPolicy(Qt::StrongFocus);
m_view->viewport()->installEventFilter(this); m_view->viewport()->installEventFilter(this);
connect(m_view, &ZoomGraphicsView::scaleChanged, m_view,
[this]() { m_view->updateProxyCacheMode(); });
m_presetDir = m_presetDir =
QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
@ -233,6 +237,108 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
presetsLayout->addWidget(savePresetBtn); presetsLayout->addWidget(savePresetBtn);
presetsLayout->addWidget(loadPresetBtn); presetsLayout->addWidget(loadPresetBtn);
presetsLayout->addSpacing(16);
auto *zoomSensLabel = new QLabel(QStringLiteral("ZOOM SENSITIVITY"));
zoomSensLabel->setStyleSheet(QStringLiteral(
"QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;"
" background: transparent; }"));
presetsLayout->addWidget(zoomSensLabel);
m_zoomSensSlider = new QSlider(Qt::Horizontal);
m_zoomSensSlider->setRange(5, 50);
m_zoomSensSlider->setValue(20);
m_zoomSensSlider->setStyleSheet(QStringLiteral(
"QSlider::groove:horizontal {"
" background: #1a1a1e; border-radius: 3px; height: 6px; }"
"QSlider::handle:horizontal {"
" background: #ecf0f6; border-radius: 5px;"
" width: 10px; margin: -4px 0; }"
"QSlider::sub-page:horizontal {"
" background: #4caf50; border-radius: 3px; }"));
m_zoomSensValue = new QLabel(QStringLiteral("1.20x"));
m_zoomSensValue->setStyleSheet(QStringLiteral(
"QLabel { color: #ecf0f6; font-size: 11px; background: transparent; }"));
m_zoomSensValue->setAlignment(Qt::AlignCenter);
connect(m_zoomSensSlider, &QSlider::valueChanged, this,
[this](int value) {
double sensitivity = 1.0 + value / 100.0;
m_view->setZoomSensitivity(sensitivity);
m_zoomSensValue->setText(
QString::number(sensitivity, 'f', 2) + QStringLiteral("x"));
scheduleSaveLayout();
});
presetsLayout->addWidget(m_zoomSensSlider);
presetsLayout->addWidget(m_zoomSensValue);
auto sliderStyle = m_zoomSensSlider->styleSheet();
auto valueLabelStyle = m_zoomSensValue->styleSheet();
presetsLayout->addSpacing(12);
auto *zoomMinLabel = new QLabel(QStringLiteral("MIN ZOOM"));
zoomMinLabel->setStyleSheet(zoomSensLabel->styleSheet());
presetsLayout->addWidget(zoomMinLabel);
m_zoomMinSlider = new QSlider(Qt::Horizontal);
m_zoomMinSlider->setRange(5, 90);
m_zoomMinSlider->setValue(30);
m_zoomMinSlider->setStyleSheet(sliderStyle);
m_zoomMinValue = new QLabel(QStringLiteral("0.30x"));
m_zoomMinValue->setStyleSheet(valueLabelStyle);
m_zoomMinValue->setAlignment(Qt::AlignCenter);
connect(m_zoomMinSlider, &QSlider::valueChanged, this,
[this](int value) {
double minZoom = value / 100.0;
if (m_zoomMaxSlider->value() <= value) {
m_zoomMaxSlider->setValue(value + 5);
}
m_view->setScaleRange(minZoom,
m_zoomMaxSlider->value() / 100.0);
m_zoomMinValue->setText(
QString::number(minZoom, 'f', 2) + QStringLiteral("x"));
scheduleSaveLayout();
});
presetsLayout->addWidget(m_zoomMinSlider);
presetsLayout->addWidget(m_zoomMinValue);
presetsLayout->addSpacing(12);
auto *zoomMaxLabel = new QLabel(QStringLiteral("MAX ZOOM"));
zoomMaxLabel->setStyleSheet(zoomSensLabel->styleSheet());
presetsLayout->addWidget(zoomMaxLabel);
m_zoomMaxSlider = new QSlider(Qt::Horizontal);
m_zoomMaxSlider->setRange(10, 500);
m_zoomMaxSlider->setValue(200);
m_zoomMaxSlider->setStyleSheet(sliderStyle);
m_zoomMaxValue = new QLabel(QStringLiteral("2.00x"));
m_zoomMaxValue->setStyleSheet(valueLabelStyle);
m_zoomMaxValue->setAlignment(Qt::AlignCenter);
connect(m_zoomMaxSlider, &QSlider::valueChanged, this,
[this](int value) {
double maxZoom = value / 100.0;
if (m_zoomMinSlider->value() >= value) {
m_zoomMinSlider->setValue(value - 5);
}
m_view->setScaleRange(m_zoomMinSlider->value() / 100.0,
maxZoom);
m_zoomMaxValue->setText(
QString::number(maxZoom, 'f', 2) + QStringLiteral("x"));
scheduleSaveLayout();
});
presetsLayout->addWidget(m_zoomMaxSlider);
presetsLayout->addWidget(m_zoomMaxValue);
presetsLayout->addStretch(); presetsLayout->addStretch();
auto *metersTab = new QWidget(); auto *metersTab = new QWidget();
@ -504,9 +610,7 @@ void GraphEditorWidget::onRefreshTimer() {
} }
void GraphEditorWidget::scheduleSaveLayout() { void GraphEditorWidget::scheduleSaveLayout() {
if (!m_saveTimer->isActive()) { m_saveTimer->start();
m_saveTimer->start();
}
} }
GraphEditorWidget::~GraphEditorWidget() { GraphEditorWidget::~GraphEditorWidget() {
@ -1130,6 +1234,9 @@ void GraphEditorWidget::saveLayoutWithViewState() {
vs.splitterGraph = sizes.value(0, 1200); vs.splitterGraph = sizes.value(0, 1200);
vs.splitterSidebar = sizes.value(1, 320); vs.splitterSidebar = sizes.value(1, 320);
vs.connectionStyle = static_cast<int>(m_connectionStyle); vs.connectionStyle = static_cast<int>(m_connectionStyle);
vs.zoomSensitivity = m_view->zoomSensitivity();
vs.zoomMin = m_zoomMinSlider->value() / 100.0;
vs.zoomMax = m_zoomMaxSlider->value() / 100.0;
vs.valid = true; vs.valid = true;
m_model->saveLayout(m_layoutPath, vs); m_model->saveLayout(m_layoutPath, vs);
} }
@ -1145,6 +1252,17 @@ void GraphEditorWidget::restoreViewState() {
if (vs.connectionStyle == static_cast<int>(ConnectionStyleType::kSquare)) { if (vs.connectionStyle == static_cast<int>(ConnectionStyleType::kSquare)) {
setConnectionStyle(ConnectionStyleType::kSquare); setConnectionStyle(ConnectionStyleType::kSquare);
} }
if (vs.zoomSensitivity > 0.0) {
m_view->setZoomSensitivity(vs.zoomSensitivity);
int sliderVal = static_cast<int>((vs.zoomSensitivity - 1.0) * 100.0);
m_zoomSensSlider->setValue(sliderVal);
}
if (vs.zoomMin > 0.0) {
m_zoomMinSlider->setValue(static_cast<int>(vs.zoomMin * 100.0));
}
if (vs.zoomMax > 0.0) {
m_zoomMaxSlider->setValue(static_cast<int>(vs.zoomMax * 100.0));
}
} else { } else {
m_view->zoomFitAll(); m_view->zoomFitAll();
} }

View file

@ -14,9 +14,10 @@
namespace QtNodes { namespace QtNodes {
using NodeId = unsigned int; using NodeId = unsigned int;
class BasicGraphicsScene; class BasicGraphicsScene;
class GraphicsView;
} // namespace QtNodes } // namespace QtNodes
class ZoomGraphicsView;
class AudioLevelMeter; class AudioLevelMeter;
class WarpGraphModel; class WarpGraphModel;
class NodeVolumeWidget; class NodeVolumeWidget;
@ -24,6 +25,7 @@ class QLabel;
class QScrollArea; class QScrollArea;
class QSplitter; class QSplitter;
class QTabWidget; class QTabWidget;
class QSlider;
class QTimer; class QTimer;
class DeleteVirtualNodeCommand; class DeleteVirtualNodeCommand;
@ -96,7 +98,7 @@ private:
warppipe::Client *m_client = nullptr; warppipe::Client *m_client = nullptr;
WarpGraphModel *m_model = nullptr; WarpGraphModel *m_model = nullptr;
QtNodes::BasicGraphicsScene *m_scene = nullptr; QtNodes::BasicGraphicsScene *m_scene = nullptr;
QtNodes::GraphicsView *m_view = nullptr; ZoomGraphicsView *m_view = nullptr;
QSplitter *m_splitter = nullptr; QSplitter *m_splitter = nullptr;
QTabWidget *m_sidebar = nullptr; QTabWidget *m_sidebar = nullptr;
QTimer *m_refreshTimer = nullptr; QTimer *m_refreshTimer = nullptr;
@ -129,4 +131,11 @@ private:
QScrollArea *m_rulesScroll = nullptr; QScrollArea *m_rulesScroll = nullptr;
ConnectionStyleType m_connectionStyle = ConnectionStyleType::kBezier; ConnectionStyleType m_connectionStyle = ConnectionStyleType::kBezier;
QSlider *m_zoomSensSlider = nullptr;
QLabel *m_zoomSensValue = nullptr;
QSlider *m_zoomMinSlider = nullptr;
QLabel *m_zoomMinValue = nullptr;
QSlider *m_zoomMaxSlider = nullptr;
QLabel *m_zoomMaxValue = nullptr;
}; };

View file

@ -29,6 +29,8 @@ WarpGraphModel::WarpGraphModel(warppipe::Client *client, QObject *parent)
if (parent) { if (parent) {
setParent(parent); setParent(parent);
} }
connect(this, &WarpGraphModel::nodeUpdated, this,
[this](QtNodes::NodeId nodeId) { m_styleCache.erase(nodeId); });
} }
QtNodes::NodeId WarpGraphModel::newNodeId() { return m_nextNodeId++; } QtNodes::NodeId WarpGraphModel::newNodeId() { return m_nextNodeId++; }
@ -195,9 +197,14 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId,
case QtNodes::NodeRole::Type: case QtNodes::NodeRole::Type:
return QString("PipeWire"); return QString("PipeWire");
case QtNodes::NodeRole::Style: { case QtNodes::NodeRole::Style: {
auto cacheIt = m_styleCache.find(nodeId);
if (cacheIt != m_styleCache.end())
return cacheIt->second;
bool ghost = m_ghostNodes.find(nodeId) != m_ghostNodes.end(); bool ghost = m_ghostNodes.find(nodeId) != m_ghostNodes.end();
WarpNodeType type = classifyNode(data.info); WarpNodeType type = classifyNode(data.info);
return styleForNode(type, ghost); QVariant result = styleForNode(type, ghost);
m_styleCache[nodeId] = result;
return result;
} }
case QtNodes::NodeRole::Widget: { case QtNodes::NodeRole::Widget: {
auto wIt = m_volumeWidgets.find(nodeId); auto wIt = m_volumeWidgets.find(nodeId);
@ -321,6 +328,7 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) {
m_positions.erase(nodeId); m_positions.erase(nodeId);
m_sizes.erase(nodeId); m_sizes.erase(nodeId);
m_volumeStates.erase(nodeId); m_volumeStates.erase(nodeId);
m_styleCache.erase(nodeId);
auto vwIt = m_volumeWidgets.find(nodeId); auto vwIt = m_volumeWidgets.find(nodeId);
if (vwIt != m_volumeWidgets.end()) { if (vwIt != m_volumeWidgets.end()) {
delete vwIt->second; delete vwIt->second;
@ -593,7 +601,8 @@ void WarpGraphModel::refreshFromClient() {
if (outIsGhost || inIsGhost) { if (outIsGhost || inIsGhost) {
m_ghostConnections.insert(connId); m_ghostConnections.insert(connId);
} else { }
{
auto connIt = m_connections.find(connId); auto connIt = m_connections.find(connId);
if (connIt != m_connections.end()) { if (connIt != m_connections.end()) {
m_connections.erase(connIt); m_connections.erase(connIt);
@ -643,10 +652,8 @@ void WarpGraphModel::refreshFromClient() {
} }
QtNodes::ConnectionId connId{outQtId, outIdx, inQtId, inIdx}; QtNodes::ConnectionId connId{outQtId, outIdx, inQtId, inIdx};
if (m_connections.find(connId) == m_connections.end()) { if (m_ghostConnections.find(connId) == m_ghostConnections.end()) {
m_connections.insert(connId);
m_ghostConnections.insert(connId); m_ghostConnections.insert(connId);
Q_EMIT connectionCreated(connId);
} }
it = m_pendingGhostConnections.erase(it); it = m_pendingGhostConnections.erase(it);
} }
@ -969,6 +976,12 @@ void WarpGraphModel::saveLayout(const QString &path,
viewObj["splitter_sidebar"] = viewState.splitterSidebar; viewObj["splitter_sidebar"] = viewState.splitterSidebar;
} }
viewObj["connection_style"] = viewState.connectionStyle; viewObj["connection_style"] = viewState.connectionStyle;
if (viewState.zoomSensitivity > 0.0)
viewObj["zoom_sensitivity"] = viewState.zoomSensitivity;
if (viewState.zoomMin > 0.0)
viewObj["zoom_min"] = viewState.zoomMin;
if (viewState.zoomMax > 0.0)
viewObj["zoom_max"] = viewState.zoomMax;
root["view"] = viewObj; root["view"] = viewObj;
} }
@ -1029,6 +1042,9 @@ bool WarpGraphModel::loadLayout(const QString &path) {
m_savedViewState.splitterGraph = viewObj["splitter_graph"].toInt(0); m_savedViewState.splitterGraph = viewObj["splitter_graph"].toInt(0);
m_savedViewState.splitterSidebar = viewObj["splitter_sidebar"].toInt(0); m_savedViewState.splitterSidebar = viewObj["splitter_sidebar"].toInt(0);
m_savedViewState.connectionStyle = viewObj["connection_style"].toInt(0); m_savedViewState.connectionStyle = viewObj["connection_style"].toInt(0);
m_savedViewState.zoomSensitivity = viewObj["zoom_sensitivity"].toDouble(0.0);
m_savedViewState.zoomMin = viewObj["zoom_min"].toDouble(0.0);
m_savedViewState.zoomMax = viewObj["zoom_max"].toDouble(0.0);
m_savedViewState.valid = true; m_savedViewState.valid = true;
} }

View file

@ -99,6 +99,9 @@ public:
double scale; double scale;
double centerX; double centerX;
double centerY; double centerY;
double zoomSensitivity;
double zoomMin;
double zoomMax;
int splitterGraph; int splitterGraph;
int splitterSidebar; int splitterSidebar;
int connectionStyle; int connectionStyle;
@ -156,4 +159,5 @@ private:
std::unordered_map<QtNodes::NodeId, NodeVolumeState> m_volumeStates; std::unordered_map<QtNodes::NodeId, NodeVolumeState> m_volumeStates;
std::unordered_map<QtNodes::NodeId, QWidget *> m_volumeWidgets; std::unordered_map<QtNodes::NodeId, QWidget *> m_volumeWidgets;
mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache;
}; };

142
gui/ZoomGraphicsView.h Normal file
View file

@ -0,0 +1,142 @@
#pragma once
#include <QtNodes/GraphicsView>
#include <QtNodes/StyleCollection>
#include <QGraphicsProxyWidget>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QWheelEvent>
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
#include <cmath>
class ZoomGraphicsView : public QtNodes::GraphicsView {
public:
explicit ZoomGraphicsView(QtNodes::BasicGraphicsScene *scene,
QWidget *parent = nullptr)
: QtNodes::GraphicsView(scene, parent) {
setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
setCacheMode(QGraphicsView::CacheNone);
}
void setZoomSensitivity(double sensitivity) {
m_sensitivity = sensitivity;
}
double zoomSensitivity() const { return m_sensitivity; }
void updateProxyCacheMode() {
if (!scene())
return;
const double zoom = transform().m11();
const bool highZoom = zoom > 1.4;
if (highZoom == m_proxiesCached)
return;
m_proxiesCached = highZoom;
auto cacheMode = highZoom ? QGraphicsItem::DeviceCoordinateCache
: QGraphicsItem::NoCache;
for (QGraphicsItem *item : scene()->items()) {
if (item->type() == QGraphicsProxyWidget::Type ||
item->type() == QtNodes::ConnectionGraphicsObject::Type)
item->setCacheMode(cacheMode);
}
}
protected:
void wheelEvent(QWheelEvent *event) override {
const double dy = event->angleDelta().y();
if (dy == 0.0) {
event->ignore();
return;
}
static constexpr double kTickUnits = 120.0;
const double exponent = dy / kTickUnits;
const double factor = std::pow(m_sensitivity, exponent);
const double proposed = transform().m11() * factor;
setupScale(proposed);
}
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
m_panActive = true;
m_panStart = event->pos();
}
QGraphicsView::mousePressEvent(event);
}
void mouseMoveEvent(QMouseEvent *event) override {
if (m_panActive && (event->buttons() & Qt::LeftButton) &&
!(event->modifiers() & Qt::ShiftModifier) &&
!scene()->mouseGrabberItem()) {
QPoint delta = event->pos() - m_panStart;
m_panStart = event->pos();
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x());
verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y());
return;
}
QGraphicsView::mouseMoveEvent(event);
}
void mouseReleaseEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton)
m_panActive = false;
QGraphicsView::mouseReleaseEvent(event);
}
void drawBackground(QPainter *painter, const QRectF &r) override {
QGraphicsView::drawBackground(painter, r);
const double zoom = transform().m11();
auto const &style =
QtNodes::StyleCollection::flowViewStyle();
static constexpr double kBaseFine = 15.0;
static constexpr double kBaseCoarse = 150.0;
static constexpr double kMinPixelSpacing = 10.0;
double fineStep = kBaseFine;
while (fineStep * zoom < kMinPixelSpacing)
fineStep *= 2.0;
double coarseStep = kBaseCoarse;
while (coarseStep * zoom < kMinPixelSpacing * 5.0)
coarseStep *= 2.0;
auto drawGrid = [&](double gridStep) {
QRect windowRect = rect();
QPointF tl = mapToScene(windowRect.topLeft());
QPointF br = mapToScene(windowRect.bottomRight());
double left = std::floor(tl.x() / gridStep - 0.5);
double right = std::floor(br.x() / gridStep + 1.0);
double bottom = std::floor(tl.y() / gridStep - 0.5);
double top = std::floor(br.y() / gridStep + 1.0);
for (int xi = int(left); xi <= int(right); ++xi)
painter->drawLine(QLineF(xi * gridStep, bottom * gridStep,
xi * gridStep, top * gridStep));
for (int yi = int(bottom); yi <= int(top); ++yi)
painter->drawLine(QLineF(left * gridStep, yi * gridStep,
right * gridStep, yi * gridStep));
};
painter->setPen(QPen(style.FineGridColor, 1.0));
drawGrid(fineStep);
painter->setPen(QPen(style.CoarseGridColor, 1.0));
drawGrid(coarseStep);
}
private:
double m_sensitivity = 1.2;
bool m_proxiesCached = false;
bool m_panActive = false;
QPoint m_panStart;
};