This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 09:15:25 -07:00
commit 750868c63f
4 changed files with 119 additions and 16 deletions

View file

@ -182,6 +182,13 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this); m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); 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( QtNodes::ConnectionStyle::setConnectionStyle(
R"({"ConnectionStyle": { R"({"ConnectionStyle": {
"ConstructionColor": "#b4b4c8", "ConstructionColor": "#b4b4c8",
@ -644,6 +651,10 @@ GraphEditorWidget::~GraphEditorWidget() {
if (m_client) { if (m_client) {
m_client->SetChangeCallback(nullptr); m_client->SetChangeCallback(nullptr);
} }
m_meterTimer->stop();
m_refreshTimer->stop();
m_changeTimer->stop();
m_saveTimer->stop();
} }
int GraphEditorWidget::nodeCount() const { int GraphEditorWidget::nodeCount() const {

View file

@ -208,8 +208,8 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId,
} }
case QtNodes::NodeRole::Widget: { case QtNodes::NodeRole::Widget: {
auto wIt = m_volumeWidgets.find(nodeId); auto wIt = m_volumeWidgets.find(nodeId);
if (wIt != m_volumeWidgets.end()) if (wIt != m_volumeWidgets.end() && wIt->second)
return QVariant::fromValue(wIt->second); return QVariant::fromValue(wIt->second.data());
return QVariant::fromValue(static_cast<QWidget *>(nullptr)); return QVariant::fromValue(static_cast<QWidget *>(nullptr));
} }
default: default:
@ -329,11 +329,7 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) {
m_sizes.erase(nodeId); m_sizes.erase(nodeId);
m_volumeStates.erase(nodeId); m_volumeStates.erase(nodeId);
m_styleCache.erase(nodeId); m_styleCache.erase(nodeId);
auto vwIt = m_volumeWidgets.find(nodeId); m_volumeWidgets.erase(nodeId);
if (vwIt != m_volumeWidgets.end()) {
delete vwIt->second;
m_volumeWidgets.erase(vwIt);
}
Q_EMIT nodeDeleted(nodeId); Q_EMIT nodeDeleted(nodeId);
return true; return true;
} }
@ -356,9 +352,12 @@ void WarpGraphModel::refreshFromClient() {
return; return;
} }
Q_EMIT beginBatchUpdate();
m_refreshing = true; m_refreshing = true;
auto nodesResult = m_client->ListNodes(); auto nodesResult = m_client->ListNodes();
if (!nodesResult.ok()) { if (!nodesResult.ok()) {
m_refreshing = false;
Q_EMIT endBatchUpdate();
return; return;
} }
@ -497,7 +496,8 @@ void WarpGraphModel::refreshFromClient() {
if (savedIt != m_savedPositions.end()) { if (savedIt != m_savedPositions.end()) {
m_positions.emplace(qtId, savedIt->second); m_positions.emplace(qtId, savedIt->second);
} else { } 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)); m_positions.emplace(qtId, findNonOverlappingPosition(candidate, nodeIt->second));
} }
} }
@ -680,8 +680,8 @@ void WarpGraphModel::refreshFromClient() {
cached.mute = mute; cached.mute = mute;
auto wIt = m_volumeWidgets.find(qtId); auto wIt = m_volumeWidgets.find(qtId);
if (wIt != m_volumeWidgets.end()) { if (wIt != m_volumeWidgets.end() && wIt->second) {
auto *vw = static_cast<NodeVolumeWidget *>(wIt->second); auto *vw = static_cast<NodeVolumeWidget *>(wIt->second.data());
if (!vw->isSliderDown()) { if (!vw->isSliderDown()) {
vw->setVolume(sliderVal); vw->setVolume(sliderVal);
vw->setMuted(mute); vw->setMuted(mute);
@ -692,6 +692,7 @@ void WarpGraphModel::refreshFromClient() {
} }
m_refreshing = false; m_refreshing = false;
Q_EMIT endBatchUpdate();
} }
const WarpNodeData * const WarpNodeData *
@ -763,8 +764,61 @@ QPointF WarpGraphModel::nextPosition(const WarpNodeData &data) {
return pos; 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<QPointF> 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, QPointF WarpGraphModel::findNonOverlappingPosition(QPointF candidate,
const WarpNodeData &data) const { const WarpNodeData &data) const {
QSizeF newSize(estimateNodeSize(data)); QSizeF newSize(estimateNodeSize(data));
constexpr int kMaxAttempts = 50; constexpr int kMaxAttempts = 50;
@ -801,6 +855,22 @@ bool WarpGraphModel::isGhost(QtNodes::NodeId nodeId) const {
return m_ghostNodes.find(nodeId) != m_ghostNodes.end(); 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<QtNodes::ConnectionId>
WarpGraphModel::allGhostConnectionIds(QtNodes::NodeId nodeId) const {
std::unordered_set<QtNodes::ConnectionId> 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 { uint32_t WarpGraphModel::findPwNodeIdByName(const std::string &name) const {
for (const auto &[qtId, data] : m_nodes) { for (const auto &[qtId, data] : m_nodes) {
if (data.info.name == name) { if (data.info.name == name) {
@ -855,8 +925,8 @@ void WarpGraphModel::setNodeVolumeState(QtNodes::NodeId nodeId,
} }
auto wIt = m_volumeWidgets.find(nodeId); auto wIt = m_volumeWidgets.find(nodeId);
if (wIt != m_volumeWidgets.end()) { if (wIt != m_volumeWidgets.end() && wIt->second) {
auto *w = qobject_cast<NodeVolumeWidget *>(wIt->second); auto *w = qobject_cast<NodeVolumeWidget *>(wIt->second.data());
if (w) { if (w) {
w->setVolume(volumeToSlider(state.volume)); w->setVolume(volumeToSlider(state.volume));
w->setMuted(state.mute); 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) { auto layoutColumn = [&](Column &col, double xOffset) {
double y = 0.0; double y = 0.0;
for (QtNodes::NodeId id : col.ids) { for (QtNodes::NodeId id : col.ids) {

View file

@ -8,8 +8,10 @@
#include <QHash> #include <QHash>
#include <QPointF> #include <QPointF>
#include <QSize> #include <QSize>
#include <QPointer>
#include <QString> #include <QString>
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@ -77,6 +79,9 @@ public:
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const; const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const; QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
bool isGhost(QtNodes::NodeId nodeId) const; bool isGhost(QtNodes::NodeId nodeId) const;
bool ghostConnectionExists(QtNodes::ConnectionId connectionId) const;
std::unordered_set<QtNodes::ConnectionId> allGhostConnectionIds(
QtNodes::NodeId nodeId) const;
void setPendingPosition(const std::string &nodeName, QPointF pos); void setPendingPosition(const std::string &nodeName, QPointF pos);
static WarpNodeType classifyNode(const warppipe::NodeInfo &info); static WarpNodeType classifyNode(const warppipe::NodeInfo &info);
@ -91,6 +96,8 @@ public:
NodeVolumeState nodeVolumeState(QtNodes::NodeId nodeId) const; NodeVolumeState nodeVolumeState(QtNodes::NodeId nodeId) const;
Q_SIGNALS: Q_SIGNALS:
void beginBatchUpdate();
void endBatchUpdate();
void nodeVolumeChanged(QtNodes::NodeId nodeId, NodeVolumeState previous, void nodeVolumeChanged(QtNodes::NodeId nodeId, NodeVolumeState previous,
NodeVolumeState current); NodeVolumeState current);
@ -120,6 +127,8 @@ private:
static QVariant styleForNode(WarpNodeType type, bool ghost); static QVariant styleForNode(WarpNodeType type, bool ghost);
QPointF nextPosition(const WarpNodeData &data); QPointF nextPosition(const WarpNodeData &data);
QPointF findNonOverlappingPosition(QPointF candidate, const WarpNodeData &data) const; QPointF findNonOverlappingPosition(QPointF candidate, const WarpNodeData &data) const;
std::optional<QPointF> findAppGroupPosition(const WarpNodeData &data) const;
static std::string appGroupKey(const warppipe::NodeInfo &info);
static QSize estimateNodeSize(const WarpNodeData &data); static QSize estimateNodeSize(const WarpNodeData &data);
warppipe::Client *m_client = nullptr; warppipe::Client *m_client = nullptr;
@ -158,6 +167,6 @@ private:
ViewState m_savedViewState{}; ViewState m_savedViewState{};
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, QPointer<QWidget>> m_volumeWidgets;
mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache; mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache;
}; };

View file

@ -728,7 +728,7 @@ TEST_CASE("ghost connections preserved when node becomes ghost") {
model.refreshFromClient(); model.refreshFromClient();
REQUIRE(model.isGhost(appQt)); REQUIRE(model.isGhost(appQt));
REQUIRE(model.connectionExists( REQUIRE(model.ghostConnectionExists(
QtNodes::ConnectionId{appQt, 0, sinkQt, 0})); QtNodes::ConnectionId{appQt, 0, sinkQt, 0}));
} }
@ -782,7 +782,7 @@ TEST_CASE("ghost connections survive save/load round-trip") {
REQUIRE(appQt2 != 0); REQUIRE(appQt2 != 0);
REQUIRE(model2.isGhost(appQt2)); REQUIRE(model2.isGhost(appQt2));
auto conns = model2.allConnectionIds(appQt2); auto conns = model2.allGhostConnectionIds(appQt2);
REQUIRE(conns.size() == 1); REQUIRE(conns.size() == 1);
auto conn = *conns.begin(); auto conn = *conns.begin();
REQUIRE(conn.outNodeId == appQt2); REQUIRE(conn.outNodeId == appQt2);