From 750868c63f9eab5ca32b27dcae08eb32e4f24d05 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 6 Feb 2026 09:15:25 -0700 Subject: [PATCH] Stuff --- gui/GraphEditorWidget.cpp | 11 ++++ gui/WarpGraphModel.cpp | 109 +++++++++++++++++++++++++++---- gui/WarpGraphModel.h | 11 +++- tests/gui/warppipe_gui_tests.cpp | 4 +- 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index 44ce6d8..38849b7 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -182,6 +182,13 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client, m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); + connect(m_model, &WarpGraphModel::beginBatchUpdate, this, [this]() { + m_scene->setItemIndexMethod(QGraphicsScene::NoIndex); + }); + connect(m_model, &WarpGraphModel::endBatchUpdate, this, [this]() { + m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); + }); + QtNodes::ConnectionStyle::setConnectionStyle( R"({"ConnectionStyle": { "ConstructionColor": "#b4b4c8", @@ -644,6 +651,10 @@ GraphEditorWidget::~GraphEditorWidget() { if (m_client) { m_client->SetChangeCallback(nullptr); } + m_meterTimer->stop(); + m_refreshTimer->stop(); + m_changeTimer->stop(); + m_saveTimer->stop(); } int GraphEditorWidget::nodeCount() const { diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index 263cec2..e5f745f 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -208,8 +208,8 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId, } case QtNodes::NodeRole::Widget: { auto wIt = m_volumeWidgets.find(nodeId); - if (wIt != m_volumeWidgets.end()) - return QVariant::fromValue(wIt->second); + if (wIt != m_volumeWidgets.end() && wIt->second) + return QVariant::fromValue(wIt->second.data()); return QVariant::fromValue(static_cast(nullptr)); } default: @@ -329,11 +329,7 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const 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; - m_volumeWidgets.erase(vwIt); - } + m_volumeWidgets.erase(nodeId); Q_EMIT nodeDeleted(nodeId); return true; } @@ -356,9 +352,12 @@ void WarpGraphModel::refreshFromClient() { return; } + Q_EMIT beginBatchUpdate(); m_refreshing = true; auto nodesResult = m_client->ListNodes(); if (!nodesResult.ok()) { + m_refreshing = false; + Q_EMIT endBatchUpdate(); return; } @@ -497,7 +496,8 @@ void WarpGraphModel::refreshFromClient() { if (savedIt != m_savedPositions.end()) { m_positions.emplace(qtId, savedIt->second); } else { - QPointF candidate = nextPosition(nodeIt->second); + auto groupPos = findAppGroupPosition(nodeIt->second); + QPointF candidate = groupPos.value_or(nextPosition(nodeIt->second)); m_positions.emplace(qtId, findNonOverlappingPosition(candidate, nodeIt->second)); } } @@ -680,8 +680,8 @@ void WarpGraphModel::refreshFromClient() { cached.mute = mute; auto wIt = m_volumeWidgets.find(qtId); - if (wIt != m_volumeWidgets.end()) { - auto *vw = static_cast(wIt->second); + if (wIt != m_volumeWidgets.end() && wIt->second) { + auto *vw = static_cast(wIt->second.data()); if (!vw->isSliderDown()) { vw->setVolume(sliderVal); vw->setMuted(mute); @@ -692,6 +692,7 @@ void WarpGraphModel::refreshFromClient() { } m_refreshing = false; + Q_EMIT endBatchUpdate(); } const WarpNodeData * @@ -763,8 +764,61 @@ QPointF WarpGraphModel::nextPosition(const WarpNodeData &data) { return pos; } +std::string WarpGraphModel::appGroupKey(const warppipe::NodeInfo &info) { + if (!info.application_name.empty()) + return info.application_name; + if (!info.process_binary.empty()) + return info.process_binary; + return {}; +} + +std::optional WarpGraphModel::findAppGroupPosition(const WarpNodeData &data) const { + WarpNodeType type = classifyNode(data.info); + if (type != WarpNodeType::kApplication) + return std::nullopt; + + std::string key = appGroupKey(data.info); + if (key.empty()) + return std::nullopt; + + double lowestBottom = -1.0; + QPointF siblingPos; + bool found = false; + + for (const auto &[existingId, existingData] : m_nodes) { + if (classifyNode(existingData.info) != WarpNodeType::kApplication) + continue; + if (appGroupKey(existingData.info) != key) + continue; + + auto posIt = m_positions.find(existingId); + if (posIt == m_positions.end()) + continue; + + QSizeF existingSize; + auto sizeIt = m_sizes.find(existingId); + if (sizeIt != m_sizes.end()) { + existingSize = QSizeF(sizeIt->second); + } else { + existingSize = QSizeF(estimateNodeSize(existingData)); + } + + double bottom = posIt->second.y() + existingSize.height(); + if (bottom > lowestBottom) { + lowestBottom = bottom; + siblingPos = posIt->second; + found = true; + } + } + + if (!found) + return std::nullopt; + + return QPointF(siblingPos.x(), lowestBottom + kVerticalGap); +} + QPointF WarpGraphModel::findNonOverlappingPosition(QPointF candidate, - const WarpNodeData &data) const { + const WarpNodeData &data) const { QSizeF newSize(estimateNodeSize(data)); constexpr int kMaxAttempts = 50; @@ -801,6 +855,22 @@ bool WarpGraphModel::isGhost(QtNodes::NodeId nodeId) const { return m_ghostNodes.find(nodeId) != m_ghostNodes.end(); } +bool WarpGraphModel::ghostConnectionExists( + QtNodes::ConnectionId connectionId) const { + return m_ghostConnections.find(connectionId) != m_ghostConnections.end(); +} + +std::unordered_set +WarpGraphModel::allGhostConnectionIds(QtNodes::NodeId nodeId) const { + std::unordered_set result; + for (const auto &conn : m_ghostConnections) { + if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) { + result.insert(conn); + } + } + return result; +} + uint32_t WarpGraphModel::findPwNodeIdByName(const std::string &name) const { for (const auto &[qtId, data] : m_nodes) { if (data.info.name == name) { @@ -855,8 +925,8 @@ void WarpGraphModel::setNodeVolumeState(QtNodes::NodeId nodeId, } auto wIt = m_volumeWidgets.find(nodeId); - if (wIt != m_volumeWidgets.end()) { - auto *w = qobject_cast(wIt->second); + if (wIt != m_volumeWidgets.end() && wIt->second) { + auto *w = qobject_cast(wIt->second.data()); if (w) { w->setVolume(volumeToSlider(state.volume)); w->setMuted(state.mute); @@ -1175,6 +1245,19 @@ void WarpGraphModel::autoArrange() { } } + std::sort(apps.ids.begin(), apps.ids.end(), + [this](QtNodes::NodeId a, QtNodes::NodeId b) { + auto itA = m_nodes.find(a); + auto itB = m_nodes.find(b); + if (itA == m_nodes.end() || itB == m_nodes.end()) + return a < b; + std::string keyA = appGroupKey(itA->second.info); + std::string keyB = appGroupKey(itB->second.info); + if (keyA != keyB) + return keyA < keyB; + return a < b; + }); + auto layoutColumn = [&](Column &col, double xOffset) { double y = 0.0; for (QtNodes::NodeId id : col.ids) { diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index 9a892a0..7c09ca4 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -8,8 +8,10 @@ #include #include #include +#include #include +#include #include #include @@ -77,6 +79,9 @@ public: const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const; QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const; bool isGhost(QtNodes::NodeId nodeId) const; + bool ghostConnectionExists(QtNodes::ConnectionId connectionId) const; + std::unordered_set allGhostConnectionIds( + QtNodes::NodeId nodeId) const; void setPendingPosition(const std::string &nodeName, QPointF pos); static WarpNodeType classifyNode(const warppipe::NodeInfo &info); @@ -91,6 +96,8 @@ public: NodeVolumeState nodeVolumeState(QtNodes::NodeId nodeId) const; Q_SIGNALS: + void beginBatchUpdate(); + void endBatchUpdate(); void nodeVolumeChanged(QtNodes::NodeId nodeId, NodeVolumeState previous, NodeVolumeState current); @@ -120,6 +127,8 @@ private: static QVariant styleForNode(WarpNodeType type, bool ghost); QPointF nextPosition(const WarpNodeData &data); QPointF findNonOverlappingPosition(QPointF candidate, const WarpNodeData &data) const; + std::optional findAppGroupPosition(const WarpNodeData &data) const; + static std::string appGroupKey(const warppipe::NodeInfo &info); static QSize estimateNodeSize(const WarpNodeData &data); warppipe::Client *m_client = nullptr; @@ -158,6 +167,6 @@ private: ViewState m_savedViewState{}; std::unordered_map m_volumeStates; - std::unordered_map m_volumeWidgets; + std::unordered_map> m_volumeWidgets; mutable std::unordered_map m_styleCache; }; diff --git a/tests/gui/warppipe_gui_tests.cpp b/tests/gui/warppipe_gui_tests.cpp index 8fd9971..9e43a2c 100644 --- a/tests/gui/warppipe_gui_tests.cpp +++ b/tests/gui/warppipe_gui_tests.cpp @@ -728,7 +728,7 @@ TEST_CASE("ghost connections preserved when node becomes ghost") { model.refreshFromClient(); REQUIRE(model.isGhost(appQt)); - REQUIRE(model.connectionExists( + REQUIRE(model.ghostConnectionExists( QtNodes::ConnectionId{appQt, 0, sinkQt, 0})); } @@ -782,7 +782,7 @@ TEST_CASE("ghost connections survive save/load round-trip") { REQUIRE(appQt2 != 0); REQUIRE(model2.isGhost(appQt2)); - auto conns = model2.allConnectionIds(appQt2); + auto conns = model2.allGhostConnectionIds(appQt2); REQUIRE(conns.size() == 1); auto conn = *conns.begin(); REQUIRE(conn.outNodeId == appQt2);