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->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 {

View file

@ -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<QWidget *>(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<NodeVolumeWidget *>(wIt->second);
if (wIt != m_volumeWidgets.end() && wIt->second) {
auto *vw = static_cast<NodeVolumeWidget *>(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,6 +764,59 @@ 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<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,
const WarpNodeData &data) const {
QSizeF newSize(estimateNodeSize(data));
@ -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<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 {
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<NodeVolumeWidget *>(wIt->second);
if (wIt != m_volumeWidgets.end() && wIt->second) {
auto *w = qobject_cast<NodeVolumeWidget *>(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) {

View file

@ -8,8 +8,10 @@
#include <QHash>
#include <QPointF>
#include <QSize>
#include <QPointer>
#include <QString>
#include <optional>
#include <unordered_map>
#include <unordered_set>
@ -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<QtNodes::ConnectionId> 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<QPointF> 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<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;
};

View file

@ -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);