diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index bfde545..1ebdb84 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -4,6 +4,7 @@ #include "SquareConnectionPainter.h" #include "VolumeWidgets.h" #include "WarpGraphModel.h" +#include "ZoomGraphicsView.h" #include #include @@ -178,6 +179,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, bool hasLayout = m_model->loadLayout(m_layoutPath); m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); + m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); QtNodes::ConnectionStyle::setConnectionStyle( R"({"ConnectionStyle": { @@ -192,10 +194,12 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, "UseDataDefinedColors": false }})"); - m_view = new QtNodes::GraphicsView(m_scene); + m_view = new ZoomGraphicsView(m_scene); m_view->setFocusPolicy(Qt::StrongFocus); m_view->viewport()->setFocusPolicy(Qt::StrongFocus); m_view->viewport()->installEventFilter(this); + connect(m_view, &ZoomGraphicsView::scaleChanged, m_view, + [this]() { m_view->updateProxyCacheMode(); }); m_presetDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + @@ -233,6 +237,108 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, presetsLayout->addWidget(savePresetBtn); 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(); auto *metersTab = new QWidget(); @@ -504,9 +610,7 @@ void GraphEditorWidget::onRefreshTimer() { } void GraphEditorWidget::scheduleSaveLayout() { - if (!m_saveTimer->isActive()) { - m_saveTimer->start(); - } + m_saveTimer->start(); } GraphEditorWidget::~GraphEditorWidget() { @@ -1130,6 +1234,9 @@ void GraphEditorWidget::saveLayoutWithViewState() { vs.splitterGraph = sizes.value(0, 1200); vs.splitterSidebar = sizes.value(1, 320); vs.connectionStyle = static_cast(m_connectionStyle); + vs.zoomSensitivity = m_view->zoomSensitivity(); + vs.zoomMin = m_zoomMinSlider->value() / 100.0; + vs.zoomMax = m_zoomMaxSlider->value() / 100.0; vs.valid = true; m_model->saveLayout(m_layoutPath, vs); } @@ -1145,6 +1252,17 @@ void GraphEditorWidget::restoreViewState() { if (vs.connectionStyle == static_cast(ConnectionStyleType::kSquare)) { setConnectionStyle(ConnectionStyleType::kSquare); } + if (vs.zoomSensitivity > 0.0) { + m_view->setZoomSensitivity(vs.zoomSensitivity); + int sliderVal = static_cast((vs.zoomSensitivity - 1.0) * 100.0); + m_zoomSensSlider->setValue(sliderVal); + } + if (vs.zoomMin > 0.0) { + m_zoomMinSlider->setValue(static_cast(vs.zoomMin * 100.0)); + } + if (vs.zoomMax > 0.0) { + m_zoomMaxSlider->setValue(static_cast(vs.zoomMax * 100.0)); + } } else { m_view->zoomFitAll(); } diff --git a/gui/GraphEditorWidget.h b/gui/GraphEditorWidget.h index c56998f..65921ad 100644 --- a/gui/GraphEditorWidget.h +++ b/gui/GraphEditorWidget.h @@ -14,9 +14,10 @@ namespace QtNodes { using NodeId = unsigned int; class BasicGraphicsScene; -class GraphicsView; } // namespace QtNodes +class ZoomGraphicsView; + class AudioLevelMeter; class WarpGraphModel; class NodeVolumeWidget; @@ -24,6 +25,7 @@ class QLabel; class QScrollArea; class QSplitter; class QTabWidget; +class QSlider; class QTimer; class DeleteVirtualNodeCommand; @@ -96,7 +98,7 @@ private: warppipe::Client *m_client = nullptr; WarpGraphModel *m_model = nullptr; QtNodes::BasicGraphicsScene *m_scene = nullptr; - QtNodes::GraphicsView *m_view = nullptr; + ZoomGraphicsView *m_view = nullptr; QSplitter *m_splitter = nullptr; QTabWidget *m_sidebar = nullptr; QTimer *m_refreshTimer = nullptr; @@ -129,4 +131,11 @@ private: QScrollArea *m_rulesScroll = nullptr; 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; }; diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index 6fcf210..263cec2 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -29,6 +29,8 @@ WarpGraphModel::WarpGraphModel(warppipe::Client *client, QObject *parent) if (parent) { setParent(parent); } + connect(this, &WarpGraphModel::nodeUpdated, this, + [this](QtNodes::NodeId nodeId) { m_styleCache.erase(nodeId); }); } QtNodes::NodeId WarpGraphModel::newNodeId() { return m_nextNodeId++; } @@ -195,9 +197,14 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId, case QtNodes::NodeRole::Type: return QString("PipeWire"); 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(); WarpNodeType type = classifyNode(data.info); - return styleForNode(type, ghost); + QVariant result = styleForNode(type, ghost); + m_styleCache[nodeId] = result; + return result; } case QtNodes::NodeRole::Widget: { auto wIt = m_volumeWidgets.find(nodeId); @@ -321,6 +328,7 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) { m_positions.erase(nodeId); m_sizes.erase(nodeId); m_volumeStates.erase(nodeId); + m_styleCache.erase(nodeId); auto vwIt = m_volumeWidgets.find(nodeId); if (vwIt != m_volumeWidgets.end()) { delete vwIt->second; @@ -593,7 +601,8 @@ void WarpGraphModel::refreshFromClient() { if (outIsGhost || inIsGhost) { m_ghostConnections.insert(connId); - } else { + } + { auto connIt = m_connections.find(connId); if (connIt != m_connections.end()) { m_connections.erase(connIt); @@ -643,10 +652,8 @@ void WarpGraphModel::refreshFromClient() { } QtNodes::ConnectionId connId{outQtId, outIdx, inQtId, inIdx}; - if (m_connections.find(connId) == m_connections.end()) { - m_connections.insert(connId); + if (m_ghostConnections.find(connId) == m_ghostConnections.end()) { m_ghostConnections.insert(connId); - Q_EMIT connectionCreated(connId); } it = m_pendingGhostConnections.erase(it); } @@ -969,6 +976,12 @@ void WarpGraphModel::saveLayout(const QString &path, viewObj["splitter_sidebar"] = viewState.splitterSidebar; } 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; } @@ -1029,6 +1042,9 @@ bool WarpGraphModel::loadLayout(const QString &path) { 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.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; } diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index 807383d..9a892a0 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -99,6 +99,9 @@ public: double scale; double centerX; double centerY; + double zoomSensitivity; + double zoomMin; + double zoomMax; int splitterGraph; int splitterSidebar; int connectionStyle; @@ -156,4 +159,5 @@ private: std::unordered_map m_volumeStates; std::unordered_map m_volumeWidgets; + mutable std::unordered_map m_styleCache; }; diff --git a/gui/ZoomGraphicsView.h b/gui/ZoomGraphicsView.h new file mode 100644 index 0000000..53106df --- /dev/null +++ b/gui/ZoomGraphicsView.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +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; +};