#include "WarpGraphModel.h" #include "VolumeWidgets.h" #include #include #include #include #include #include #include #include #include #include #include namespace { inline int volumeToSlider(float volume) { return static_cast(std::round(std::cbrt(volume) * 100.0f)); } } #include #include WarpGraphModel::WarpGraphModel(warppipe::Client *client, QObject *parent) : QtNodes::AbstractGraphModel(), m_client(client) { if (parent) { setParent(parent); } connect(this, &WarpGraphModel::nodeUpdated, this, [this](QtNodes::NodeId nodeId) { m_styleCache.erase(nodeId); }); } QtNodes::NodeId WarpGraphModel::newNodeId() { return m_nextNodeId++; } std::unordered_set WarpGraphModel::allNodeIds() const { std::unordered_set ids; ids.reserve(m_nodes.size()); for (const auto &entry : m_nodes) { ids.insert(entry.first); } return ids; } std::unordered_set WarpGraphModel::allConnectionIds(QtNodes::NodeId const nodeId) const { std::unordered_set result; for (const auto &conn : m_connections) { if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) { result.insert(conn); } } return result; } std::unordered_set WarpGraphModel::connections(QtNodes::NodeId nodeId, QtNodes::PortType portType, QtNodes::PortIndex portIndex) const { std::unordered_set result; for (const auto &conn : m_connections) { if (portType == QtNodes::PortType::Out) { if (conn.outNodeId == nodeId && conn.outPortIndex == portIndex) { result.insert(conn); } } else if (portType == QtNodes::PortType::In) { if (conn.inNodeId == nodeId && conn.inPortIndex == portIndex) { result.insert(conn); } } } return result; } bool WarpGraphModel::connectionExists( QtNodes::ConnectionId const connectionId) const { return m_connections.find(connectionId) != m_connections.end(); } QtNodes::NodeId WarpGraphModel::addNode(QString const) { return newNodeId(); } bool WarpGraphModel::connectionPossible( QtNodes::ConnectionId const connectionId) const { if (!nodeExists(connectionId.outNodeId) || !nodeExists(connectionId.inNodeId)) { return false; } if (connectionExists(connectionId)) { return false; } if (m_ghostNodes.count(connectionId.outNodeId) || m_ghostNodes.count(connectionId.inNodeId)) { return false; } auto outIt = m_nodes.find(connectionId.outNodeId); auto inIt = m_nodes.find(connectionId.inNodeId); if (outIt == m_nodes.end() || inIt == m_nodes.end()) { return false; } auto outIdx = static_cast(connectionId.outPortIndex); auto inIdx = static_cast(connectionId.inPortIndex); if (outIdx >= outIt->second.outputPorts.size()) { return false; } if (inIdx >= inIt->second.inputPorts.size()) { return false; } WarpNodeType outType = classifyNode(outIt->second.info); WarpNodeType inType = classifyNode(inIt->second.info); bool outIsVideo = (outType == WarpNodeType::kVideoSource || outType == WarpNodeType::kVideoSink); bool inIsVideo = (inType == WarpNodeType::kVideoSource || inType == WarpNodeType::kVideoSink); if (outIsVideo != inIsVideo) { return false; } return true; } void WarpGraphModel::addConnection( QtNodes::ConnectionId const connectionId) { if (!connectionPossible(connectionId)) { return; } if (m_client) { auto outGroupIt = m_appGroups.find(connectionId.outNodeId); auto inGroupIt = m_appGroups.find(connectionId.inNodeId); std::vector outPorts; std::vector inPorts; if (outGroupIt != m_appGroups.end()) { auto mapIt = outGroupIt->second.outputPortMap.find( static_cast(connectionId.outPortIndex)); if (mapIt != outGroupIt->second.outputPortMap.end()) outPorts = mapIt->second; } else { auto outIt = m_nodes.find(connectionId.outNodeId); if (outIt != m_nodes.end()) { auto idx = static_cast(connectionId.outPortIndex); if (idx < outIt->second.outputPorts.size()) outPorts.push_back(outIt->second.outputPorts[idx].id); } } if (inGroupIt != m_appGroups.end()) { auto mapIt = inGroupIt->second.inputPortMap.find( static_cast(connectionId.inPortIndex)); if (mapIt != inGroupIt->second.inputPortMap.end()) inPorts = mapIt->second; } else { auto inIt = m_nodes.find(connectionId.inNodeId); if (inIt != m_nodes.end()) { auto idx = static_cast(connectionId.inPortIndex); if (idx < inIt->second.inputPorts.size()) inPorts.push_back(inIt->second.inputPorts[idx].id); } } if (outPorts.empty() || inPorts.empty()) return; bool anyCreated = false; for (const auto &outPortId : outPorts) { for (const auto &inPortId : inPorts) { auto result = m_client->CreateLink( outPortId, inPortId, warppipe::LinkOptions{.linger = true}); if (result.ok()) { m_linkIdToConn.emplace(result.value.id.value, connectionId); anyCreated = true; } } } if (!anyCreated) return; } m_connections.insert(connectionId); Q_EMIT connectionCreated(connectionId); } bool WarpGraphModel::nodeExists(QtNodes::NodeId const nodeId) const { return m_nodes.find(nodeId) != m_nodes.end(); } QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole role) const { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return QVariant(); } const auto &data = it->second; switch (role) { case QtNodes::NodeRole::Caption: { QString caption = captionForNode(data.info); auto groupIt = m_appGroups.find(nodeId); if (groupIt != m_appGroups.end()) { int count = static_cast(groupIt->second.memberPwIds.size()); if (count > 1) caption += QStringLiteral(" (%1 streams)").arg(count); } return caption; } case QtNodes::NodeRole::CaptionVisible: return true; case QtNodes::NodeRole::Position: { auto posIt = m_positions.find(nodeId); if (posIt != m_positions.end()) { return posIt->second; } return QPointF(0, 0); } case QtNodes::NodeRole::Size: { auto sizeIt = m_sizes.find(nodeId); if (sizeIt != m_sizes.end()) { return sizeIt->second; } return estimateNodeSize(data); } case QtNodes::NodeRole::InPortCount: return static_cast(data.inputPorts.size()); case QtNodes::NodeRole::OutPortCount: return static_cast(data.outputPorts.size()); 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); QVariant result = styleForNode(type, ghost); m_styleCache[nodeId] = result; return result; } case QtNodes::NodeRole::Widget: { auto wIt = m_volumeWidgets.find(nodeId); if (wIt != m_volumeWidgets.end() && wIt->second) return QVariant::fromValue(wIt->second.data()); return QVariant::fromValue(static_cast(nullptr)); } default: return QVariant(); } } bool WarpGraphModel::setNodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole role, QVariant value) { if (!nodeExists(nodeId)) { return false; } if (role == QtNodes::NodeRole::Position) { m_positions[nodeId] = value.toPointF(); Q_EMIT nodePositionUpdated(nodeId); return true; } if (role == QtNodes::NodeRole::Size) { m_sizes[nodeId] = value.toSize(); return true; } return false; } QVariant WarpGraphModel::portData(QtNodes::NodeId nodeId, QtNodes::PortType portType, QtNodes::PortIndex portIndex, QtNodes::PortRole role) const { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return QVariant(); } const auto &data = it->second; if (role == QtNodes::PortRole::DataType) { WarpNodeType ntype = classifyNode(data.info); if (ntype == WarpNodeType::kVideoSource || ntype == WarpNodeType::kVideoSink) return QString("video"); return QString("audio"); } if (role == QtNodes::PortRole::CaptionVisible) { return true; } if (role == QtNodes::PortRole::Caption) { if (portType == QtNodes::PortType::In) { auto idx = static_cast(portIndex); if (idx < data.inputPorts.size()) { return QString::fromStdString(data.inputPorts[idx].name); } } else if (portType == QtNodes::PortType::Out) { auto idx = static_cast(portIndex); if (idx < data.outputPorts.size()) { return QString::fromStdString(data.outputPorts[idx].name); } } } if (role == QtNodes::PortRole::ConnectionPolicyRole) { return QVariant::fromValue(QtNodes::ConnectionPolicy::Many); } return QVariant(); } bool WarpGraphModel::setPortData(QtNodes::NodeId, QtNodes::PortType, QtNodes::PortIndex, QVariant const &, QtNodes::PortRole) { return false; } bool WarpGraphModel::deleteConnection( QtNodes::ConnectionId const connectionId) { auto it = m_connections.find(connectionId); if (it == m_connections.end()) { return false; } if (m_client && !m_refreshing) { std::vector linksToRemove; for (const auto &[linkId, connId] : m_linkIdToConn) { if (connId == connectionId) linksToRemove.push_back(linkId); } for (uint32_t linkId : linksToRemove) { m_client->RemoveLink(warppipe::LinkId{linkId}); m_linkIdToConn.erase(linkId); } } m_connections.erase(it); m_connectionChannels.erase(connectionId); recomputeConnectionChannels(); Q_EMIT connectionDeleted(connectionId); return true; } bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) { if (!nodeExists(nodeId)) { return false; } std::vector toRemove; for (const auto &conn : m_connections) { if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) { toRemove.push_back(conn); } } for (const auto &conn : toRemove) { deleteConnection(conn); } auto groupIt = m_appGroups.find(nodeId); if (groupIt != m_appGroups.end()) { for (uint32_t memberPwId : groupIt->second.memberPwIds) m_pwToGroupQt.erase(memberPwId); m_groupKeyToQt.erase(groupIt->second.groupKey); m_appGroups.erase(groupIt); } m_nodes.erase(nodeId); m_positions.erase(nodeId); m_sizes.erase(nodeId); m_volumeStates.erase(nodeId); m_peakLevels.erase(nodeId); m_styleCache.erase(nodeId); m_volumeWidgets.erase(nodeId); Q_EMIT nodeDeleted(nodeId); return true; } QJsonObject WarpGraphModel::saveNode(QtNodes::NodeId const nodeId) const { QJsonObject obj; obj["id"] = static_cast(nodeId); QPointF pos = nodeData(nodeId, QtNodes::NodeRole::Position).toPointF(); QJsonObject posObj; posObj["x"] = pos.x(); posObj["y"] = pos.y(); obj["position"] = posObj; return obj; } void WarpGraphModel::loadNode(QJsonObject const &) {} void WarpGraphModel::rebuildGroupPortMap(QtNodes::NodeId groupQtId) { auto groupIt = m_appGroups.find(groupQtId); if (groupIt == m_appGroups.end()) return; auto &group = groupIt->second; group.outputPortMap.clear(); group.inputPortMap.clear(); auto nodeIt = m_nodes.find(groupQtId); if (nodeIt == m_nodes.end()) return; const auto &canonOut = nodeIt->second.outputPorts; const auto &canonIn = nodeIt->second.inputPorts; for (uint32_t memberPwId : group.memberPwIds) { auto memberPorts = m_client->ListPorts(warppipe::NodeId{memberPwId}); if (!memberPorts.ok()) continue; for (const auto &port : memberPorts.value) { if (!port.is_input) { for (size_t ci = 0; ci < canonOut.size(); ++ci) { if (port.name == canonOut[ci].name) { group.outputPortMap[static_cast(ci)].push_back(port.id); m_portToGroupPort[port.id.value] = {groupQtId, static_cast(ci), false}; break; } } } else { for (size_t ci = 0; ci < canonIn.size(); ++ci) { if (port.name == canonIn[ci].name) { group.inputPortMap[static_cast(ci)].push_back(port.id); m_portToGroupPort[port.id.value] = {groupQtId, static_cast(ci), true}; break; } } } } } } void WarpGraphModel::refreshFromClient() { if (!m_client) { return; } m_refreshing = true; bool sceneChanged = false; auto nodesResult = m_client->ListNodes(); if (!nodesResult.ok()) { m_refreshing = false; return; } std::unordered_set seenPwIds; // Phase 1: Separate app streams (to be grouped) from other nodes. std::unordered_map> appStreams; std::vector nonAppNodes; for (const auto &nodeInfo : nodesResult.value) { seenPwIds.insert(nodeInfo.id.value); WarpNodeType nodeType = classifyNode(nodeInfo); if (nodeType == WarpNodeType::kApplication) { if (nodeInfo.name.empty() && nodeInfo.application_name.empty()) continue; std::string key = appGroupKey(nodeInfo); if (key.empty()) key = nodeInfo.name; appStreams[key].push_back(nodeInfo); } else { nonAppNodes.push_back(nodeInfo); } } // Phase 2: Process non-app nodes (unchanged logic). for (const auto &nodeInfo : nonAppNodes) { auto existing = m_pwToQt.find(nodeInfo.id.value); if (existing != m_pwToQt.end()) { QtNodes::NodeId qtId = existing->second; auto &data = m_nodes[qtId]; bool typeChanged = (data.info.is_virtual != nodeInfo.is_virtual); data.info = nodeInfo; if (typeChanged) { m_styleCache.erase(qtId); Q_EMIT nodeUpdated(qtId); } bool portsMissing = data.inputPorts.empty() && data.outputPorts.empty(); if (portsMissing) { auto portsResult = m_client->ListPorts(nodeInfo.id); if (portsResult.ok() && !portsResult.value.empty()) { for (const auto &port : portsResult.value) { if (port.is_input) { data.inputPorts.push_back(port); } else { data.outputPorts.push_back(port); } } std::sort(data.inputPorts.begin(), data.inputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(data.outputPorts.begin(), data.outputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); Q_EMIT nodeUpdated(qtId); } } if (m_ghostNodes.erase(qtId)) { std::erase_if(m_ghostConnections, [&](const auto &gc) { if (gc.outNodeId != qtId && gc.inNodeId != qtId) return false; m_connections.erase(gc); Q_EMIT connectionDeleted(gc); return true; }); Q_EMIT nodeUpdated(qtId); } continue; } // Ghost matching for non-app nodes (rare but possible). QtNodes::NodeId ghostMatch = 0; std::string nodeName = nodeInfo.name; for (const auto &ghostId : m_ghostNodes) { if (m_appGroups.count(ghostId)) continue; auto ghostIt = m_nodes.find(ghostId); if (ghostIt != m_nodes.end() && ghostIt->second.info.name == nodeName) { ghostMatch = ghostId; break; } } if (ghostMatch != 0) { m_ghostNodes.erase(ghostMatch); std::erase_if(m_ghostConnections, [&](const auto &gc) { if (gc.outNodeId != ghostMatch && gc.inNodeId != ghostMatch) return false; m_connections.erase(gc); Q_EMIT connectionDeleted(gc); return true; }); m_pwToQt.emplace(nodeInfo.id.value, ghostMatch); auto &data = m_nodes[ghostMatch]; data.info = nodeInfo; auto portsResult = m_client->ListPorts(nodeInfo.id); if (portsResult.ok()) { data.inputPorts.clear(); data.outputPorts.clear(); for (const auto &port : portsResult.value) { if (port.is_input) { data.inputPorts.push_back(port); } else { data.outputPorts.push_back(port); } } std::sort(data.inputPorts.begin(), data.inputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(data.outputPorts.begin(), data.outputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); } Q_EMIT nodeUpdated(ghostMatch); continue; } WarpNodeType nodeType = classifyNode(nodeInfo); auto portsResult = m_client->ListPorts(nodeInfo.id); std::vector inputs; std::vector outputs; if (portsResult.ok()) { for (const auto &port : portsResult.value) { if (port.is_input) { inputs.push_back(port); } else { outputs.push_back(port); } } std::sort(inputs.begin(), inputs.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(outputs.begin(), outputs.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); } QtNodes::NodeId qtId = newNodeId(); WarpNodeData data; data.info = nodeInfo; data.inputPorts = std::move(inputs); data.outputPorts = std::move(outputs); auto [nodeIt, _] = m_nodes.emplace(qtId, std::move(data)); m_pwToQt.emplace(nodeInfo.id.value, qtId); auto pendingIt = m_pendingPositions.find(nodeInfo.name); if (pendingIt != m_pendingPositions.end()) { m_positions.emplace(qtId, pendingIt->second); m_pendingPositions.erase(pendingIt); } else { auto savedIt = m_savedPositions.find(nodeInfo.name); if (savedIt != m_savedPositions.end()) { m_positions.emplace(qtId, savedIt->second); } else { QPointF candidate = nextPosition(nodeIt->second); m_positions.emplace(qtId, findNonOverlappingPosition(candidate, nodeIt->second)); } } if (nodeHasVolume(nodeType)) { auto *volumeWidget = new NodeVolumeWidget(); m_volumeWidgets[qtId] = volumeWidget; m_volumeStates[qtId] = {}; } if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } Q_EMIT nodeCreated(qtId); } // Phase 3: Process app-stream groups. std::unordered_set seenGroupKeys; for (auto &[key, members] : appStreams) { seenGroupKeys.insert(key); auto existingGroup = m_groupKeyToQt.find(key); if (existingGroup != m_groupKeyToQt.end()) { // Group already exists — update membership. QtNodes::NodeId groupQtId = existingGroup->second; auto &group = m_appGroups[groupQtId]; // Clear old reverse mappings. for (uint32_t oldPwId : group.memberPwIds) m_pwToGroupQt.erase(oldPwId); group.memberPwIds.clear(); for (const auto &m : members) { group.memberPwIds.push_back(m.id.value); m_pwToGroupQt[m.id.value] = groupQtId; } // Derive canonical ports from first member if node has no ports yet. auto &nodeData = m_nodes[groupQtId]; nodeData.info = members.front(); nodeData.info.id = warppipe::NodeId{0}; bool portsMissing = nodeData.inputPorts.empty() && nodeData.outputPorts.empty(); if (portsMissing && !members.empty()) { auto portsResult = m_client->ListPorts(members.front().id); if (portsResult.ok()) { for (const auto &port : portsResult.value) { warppipe::PortInfo canonical = port; canonical.id = warppipe::PortId{0}; canonical.node = warppipe::NodeId{0}; if (port.is_input) nodeData.inputPorts.push_back(canonical); else nodeData.outputPorts.push_back(canonical); } std::sort(nodeData.inputPorts.begin(), nodeData.inputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(nodeData.outputPorts.begin(), nodeData.outputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); } } // Un-ghost if it was ghosted. if (m_ghostNodes.erase(groupQtId)) { std::erase_if(m_ghostConnections, [&](const auto &gc) { if (gc.outNodeId != groupQtId && gc.inNodeId != groupQtId) return false; m_connections.erase(gc); Q_EMIT connectionDeleted(gc); return true; }); Q_EMIT nodeUpdated(groupQtId); } rebuildGroupPortMap(groupQtId); Q_EMIT nodeUpdated(groupQtId); continue; } // Check if any member was previously an individual node — migrate it. QtNodes::NodeId migratedQtId = 0; QPointF migratedPos; for (const auto &m : members) { auto indvIt = m_pwToQt.find(m.id.value); if (indvIt != m_pwToQt.end()) { if (migratedQtId == 0) { migratedQtId = indvIt->second; auto posIt = m_positions.find(migratedQtId); if (posIt != m_positions.end()) migratedPos = posIt->second; } if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } QtNodes::NodeId oldQt = indvIt->second; m_pwToQt.erase(indvIt); deleteNode(oldQt); } } // Check for a ghost group match. QtNodes::NodeId ghostMatch = 0; std::string groupLayoutKey = "group:" + key; for (const auto &ghostId : m_ghostNodes) { if (m_appGroups.count(ghostId)) { auto &gd = m_appGroups[ghostId]; if (gd.groupKey == key) { ghostMatch = ghostId; break; } } } if (ghostMatch != 0) { m_ghostNodes.erase(ghostMatch); std::erase_if(m_ghostConnections, [&](const auto &gc) { if (gc.outNodeId != ghostMatch && gc.inNodeId != ghostMatch) return false; m_connections.erase(gc); Q_EMIT connectionDeleted(gc); return true; }); auto &group = m_appGroups[ghostMatch]; for (uint32_t oldPwId : group.memberPwIds) m_pwToGroupQt.erase(oldPwId); group.memberPwIds.clear(); for (const auto &m : members) { group.memberPwIds.push_back(m.id.value); m_pwToGroupQt[m.id.value] = ghostMatch; } auto &nodeData = m_nodes[ghostMatch]; nodeData.info = members.front(); nodeData.info.id = warppipe::NodeId{0}; auto portsResult = m_client->ListPorts(members.front().id); if (portsResult.ok()) { nodeData.inputPorts.clear(); nodeData.outputPorts.clear(); for (const auto &port : portsResult.value) { warppipe::PortInfo canonical = port; canonical.id = warppipe::PortId{0}; canonical.node = warppipe::NodeId{0}; if (port.is_input) nodeData.inputPorts.push_back(canonical); else nodeData.outputPorts.push_back(canonical); } std::sort(nodeData.inputPorts.begin(), nodeData.inputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(nodeData.outputPorts.begin(), nodeData.outputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); } m_groupKeyToQt[key] = ghostMatch; rebuildGroupPortMap(ghostMatch); Q_EMIT nodeUpdated(ghostMatch); continue; } // Create new group visual node. QtNodes::NodeId groupQtId = newNodeId(); warppipe::NodeInfo synth = members.front(); synth.id = warppipe::NodeId{0}; WarpNodeData data; data.info = synth; // Derive canonical ports from first member. auto portsResult = m_client->ListPorts(members.front().id); if (portsResult.ok()) { for (const auto &port : portsResult.value) { warppipe::PortInfo canonical = port; canonical.id = warppipe::PortId{0}; canonical.node = warppipe::NodeId{0}; if (port.is_input) data.inputPorts.push_back(canonical); else data.outputPorts.push_back(canonical); } std::sort(data.inputPorts.begin(), data.inputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); std::sort(data.outputPorts.begin(), data.outputPorts.end(), [](const auto &a, const auto &b) { return a.name < b.name; }); } m_nodes.emplace(groupQtId, std::move(data)); AppGroupData group; group.groupKey = key; for (const auto &m : members) { group.memberPwIds.push_back(m.id.value); m_pwToGroupQt[m.id.value] = groupQtId; } m_appGroups[groupQtId] = std::move(group); m_groupKeyToQt[key] = groupQtId; // Position: migrated, pending, saved, or auto. if (migratedQtId != 0) { m_positions.emplace(groupQtId, migratedPos); } else { auto pendingIt = m_pendingPositions.find(groupLayoutKey); if (pendingIt != m_pendingPositions.end()) { m_positions.emplace(groupQtId, pendingIt->second); m_pendingPositions.erase(pendingIt); } else { auto savedIt = m_savedPositions.find(groupLayoutKey); if (savedIt != m_savedPositions.end()) { m_positions.emplace(groupQtId, savedIt->second); } else { auto savedByKey = m_savedPositions.find(key); if (savedByKey != m_savedPositions.end()) { m_positions.emplace(groupQtId, savedByKey->second); } else { QPointF candidate = nextPosition(m_nodes[groupQtId]); m_positions.emplace( groupQtId, findNonOverlappingPosition(candidate, m_nodes[groupQtId])); } } } } auto *volumeWidget = new NodeVolumeWidget(); m_volumeWidgets[groupQtId] = volumeWidget; m_volumeStates[groupQtId] = {}; rebuildGroupPortMap(groupQtId); if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } Q_EMIT nodeCreated(groupQtId); } // Phase 4a: Handle disappeared non-app PW IDs. std::vector disappearedPwIds; for (const auto &[pwId, qtId] : m_pwToQt) { if (seenPwIds.find(pwId) == seenPwIds.end()) { disappearedPwIds.push_back(pwId); } } for (uint32_t pwId : disappearedPwIds) { auto it = m_pwToQt.find(pwId); if (it == m_pwToQt.end()) continue; if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } QtNodes::NodeId qtId = it->second; m_pwToQt.erase(it); if (m_nodes.count(qtId)) deleteNode(qtId); } // Phase 4b: Handle disappeared group PW members. std::vector disappearedGroupPwIds; for (const auto &[pwId, groupQtId] : m_pwToGroupQt) { if (seenPwIds.find(pwId) == seenPwIds.end()) disappearedGroupPwIds.push_back(pwId); } for (uint32_t pwId : disappearedGroupPwIds) { auto it = m_pwToGroupQt.find(pwId); if (it == m_pwToGroupQt.end()) continue; QtNodes::NodeId groupQtId = it->second; m_pwToGroupQt.erase(it); auto groupIt = m_appGroups.find(groupQtId); if (groupIt != m_appGroups.end()) { bool anyMemberAlive = false; for (uint32_t mid : groupIt->second.memberPwIds) { if (mid != pwId && m_pwToGroupQt.count(mid)) anyMemberAlive = true; } if (!anyMemberAlive) { if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } m_ghostNodes.insert(groupQtId); Q_EMIT nodeUpdated(groupQtId); } else { auto &memberIds = groupIt->second.memberPwIds; memberIds.erase( std::remove(memberIds.begin(), memberIds.end(), pwId), memberIds.end()); rebuildGroupPortMap(groupQtId); Q_EMIT nodeUpdated(groupQtId); } } } // Phase 4c: Remove groups whose keys no longer appear. std::vector staleGroupKeys; for (const auto &[key, groupQtId] : m_groupKeyToQt) { if (seenGroupKeys.find(key) == seenGroupKeys.end()) { auto groupIt = m_appGroups.find(groupQtId); if (groupIt != m_appGroups.end() && groupIt->second.memberPwIds.empty()) { staleGroupKeys.push_back(key); } } } for (const auto &key : staleGroupKeys) { auto it = m_groupKeyToQt.find(key); if (it == m_groupKeyToQt.end()) continue; QtNodes::NodeId groupQtId = it->second; bool alreadyGhost = m_ghostNodes.count(groupQtId) > 0; if (!alreadyGhost) { if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } m_ghostNodes.insert(groupQtId); Q_EMIT nodeUpdated(groupQtId); } } // Phase 5: Sync links. auto linksResult = m_client->ListLinks(); if (linksResult.ok()) { std::unordered_set seenLinkIds; for (const auto &link : linksResult.value) { seenLinkIds.insert(link.id.value); if (m_linkIdToConn.find(link.id.value) != m_linkIdToConn.end()) continue; QtNodes::NodeId outQtId = 0; QtNodes::NodeId inQtId = 0; QtNodes::PortIndex outPortIdx = 0; QtNodes::PortIndex inPortIdx = 0; bool outFound = false; bool inFound = false; // Check group port map first. auto outGroupIt = m_portToGroupPort.find(link.output_port.value); if (outGroupIt != m_portToGroupPort.end() && !outGroupIt->second.isInput) { outQtId = outGroupIt->second.groupQtId; outPortIdx = outGroupIt->second.portIndex; outFound = true; } auto inGroupIt = m_portToGroupPort.find(link.input_port.value); if (inGroupIt != m_portToGroupPort.end() && inGroupIt->second.isInput) { inQtId = inGroupIt->second.groupQtId; inPortIdx = inGroupIt->second.portIndex; inFound = true; } // Fall back to individual node port scan. if (!outFound || !inFound) { for (const auto &[qtId, nodeData] : m_nodes) { if (m_appGroups.count(qtId)) continue; if (!outFound) { for (size_t i = 0; i < nodeData.outputPorts.size(); ++i) { if (nodeData.outputPorts[i].id.value == link.output_port.value) { auto pwIt = m_pwToQt.find(nodeData.info.id.value); if (pwIt != m_pwToQt.end()) { outQtId = pwIt->second; outPortIdx = static_cast(i); outFound = true; } break; } } } if (!inFound) { for (size_t i = 0; i < nodeData.inputPorts.size(); ++i) { if (nodeData.inputPorts[i].id.value == link.input_port.value) { auto pwIt = m_pwToQt.find(nodeData.info.id.value); if (pwIt != m_pwToQt.end()) { inQtId = pwIt->second; inPortIdx = static_cast(i); inFound = true; } break; } } } if (outFound && inFound) break; } } if (outFound && inFound) { QtNodes::ConnectionId connId{outQtId, outPortIdx, inQtId, inPortIdx}; if (m_connections.find(connId) == m_connections.end()) { if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } m_connections.insert(connId); Q_EMIT connectionCreated(connId); } m_linkIdToConn.emplace(link.id.value, connId); } } std::vector staleLinkIds; for (const auto &[linkId, connId] : m_linkIdToConn) { if (seenLinkIds.find(linkId) == seenLinkIds.end()) staleLinkIds.push_back(linkId); } for (uint32_t linkId : staleLinkIds) { auto it = m_linkIdToConn.find(linkId); if (it == m_linkIdToConn.end()) continue; QtNodes::ConnectionId connId = it->second; m_linkIdToConn.erase(it); // Only remove visual connection if no other PW links map to it. bool otherLinkExists = false; for (const auto &[otherId, otherConn] : m_linkIdToConn) { if (otherConn == connId) { otherLinkExists = true; break; } } if (!otherLinkExists) { bool outIsGhost = m_ghostNodes.find(connId.outNodeId) != m_ghostNodes.end(); bool inIsGhost = m_ghostNodes.find(connId.inNodeId) != m_ghostNodes.end(); if (outIsGhost || inIsGhost) m_ghostConnections.insert(connId); auto connIt = m_connections.find(connId); if (connIt != m_connections.end()) { if (!sceneChanged) { sceneChanged = true; Q_EMIT beginBatchUpdate(); } m_connections.erase(connIt); Q_EMIT connectionDeleted(connId); } } } } // Phase 6: Pending ghost connections. if (!m_pendingGhostConnections.empty()) { auto it = m_pendingGhostConnections.begin(); while (it != m_pendingGhostConnections.end()) { QtNodes::NodeId outQtId = 0; QtNodes::NodeId inQtId = 0; for (const auto &[qtId, data] : m_nodes) { if (data.info.name == it->outNodeName) outQtId = qtId; if (data.info.name == it->inNodeName) inQtId = qtId; } if (outQtId == 0 || inQtId == 0) { ++it; continue; } auto outNodeIt = m_nodes.find(outQtId); auto inNodeIt = m_nodes.find(inQtId); QtNodes::PortIndex outIdx = -1; QtNodes::PortIndex inIdx = -1; for (size_t i = 0; i < outNodeIt->second.outputPorts.size(); ++i) { if (outNodeIt->second.outputPorts[i].name == it->outPortName) { outIdx = static_cast(i); break; } } for (size_t i = 0; i < inNodeIt->second.inputPorts.size(); ++i) { if (inNodeIt->second.inputPorts[i].name == it->inPortName) { inIdx = static_cast(i); break; } } if (outIdx < 0 || inIdx < 0) { ++it; continue; } QtNodes::ConnectionId connId{outQtId, outIdx, inQtId, inIdx}; if (m_ghostConnections.find(connId) == m_ghostConnections.end()) m_ghostConnections.insert(connId); it = m_pendingGhostConnections.erase(it); } } // Phase 7: Volume sync. // Non-app nodes. for (const auto &[pwId, qtId] : m_pwToQt) { auto volResult = m_client->GetNodeVolume(warppipe::NodeId{pwId}); if (!volResult.ok()) continue; float vol = volResult.value.volume; bool mute = volResult.value.mute; int sliderVal = volumeToSlider(vol); sliderVal = std::clamp(sliderVal, 0, 150); auto stateIt = m_volumeStates.find(qtId); if (stateIt == m_volumeStates.end()) continue; NodeVolumeState &cached = stateIt->second; bool changed = (std::abs(cached.volume - vol) > 1e-4f) || (cached.mute != mute); if (!changed) continue; NodeVolumeState previous = cached; cached.volume = vol; cached.mute = mute; auto wIt = m_volumeWidgets.find(qtId); if (wIt != m_volumeWidgets.end() && wIt->second) { auto *vw = static_cast(wIt->second.data()); if (!vw->isSliderDown()) { vw->setVolume(sliderVal); vw->setMuted(mute); } } Q_EMIT nodeVolumeChanged(qtId, previous, cached); } // Group nodes: aggregate volume from first member. for (const auto &[groupQtId, group] : m_appGroups) { if (group.memberPwIds.empty()) continue; auto volResult = m_client->GetNodeVolume(warppipe::NodeId{group.memberPwIds.front()}); if (!volResult.ok()) continue; float vol = volResult.value.volume; bool mute = volResult.value.mute; int sliderVal = volumeToSlider(vol); sliderVal = std::clamp(sliderVal, 0, 150); auto stateIt = m_volumeStates.find(groupQtId); if (stateIt == m_volumeStates.end()) continue; NodeVolumeState &cached = stateIt->second; bool changed = (std::abs(cached.volume - vol) > 1e-4f) || (cached.mute != mute); if (!changed) continue; NodeVolumeState previous = cached; cached.volume = vol; cached.mute = mute; auto wIt = m_volumeWidgets.find(groupQtId); if (wIt != m_volumeWidgets.end() && wIt->second) { auto *vw = static_cast(wIt->second.data()); if (!vw->isSliderDown()) { vw->setVolume(sliderVal); vw->setMuted(mute); } } Q_EMIT nodeVolumeChanged(groupQtId, previous, cached); } recomputeConnectionChannels(); m_refreshing = false; if (sceneChanged) { Q_EMIT endBatchUpdate(); } } const WarpNodeData * WarpGraphModel::warpNodeData(QtNodes::NodeId nodeId) const { auto it = m_nodes.find(nodeId); if (it != m_nodes.end()) { return &it->second; } return nullptr; } QtNodes::NodeId WarpGraphModel::qtNodeIdForPw(uint32_t pwNodeId) const { auto it = m_pwToQt.find(pwNodeId); if (it != m_pwToQt.end()) return it->second; auto groupIt = m_pwToGroupQt.find(pwNodeId); if (groupIt != m_pwToGroupQt.end()) return groupIt->second; return 0; } void WarpGraphModel::setPendingPosition(const std::string &nodeName, QPointF pos) { m_pendingPositions[nodeName] = pos; } QString WarpGraphModel::captionForNode(const warppipe::NodeInfo &info) { if (!info.description.empty()) { return QString::fromStdString(info.description); } if (!info.application_name.empty() && info.application_name != info.name) { return QString::fromStdString(info.application_name); } return QString::fromStdString(info.name); } QSize WarpGraphModel::estimateNodeSize(const WarpNodeData &data) { int maxPorts = static_cast( std::max(data.inputPorts.size(), data.outputPorts.size())); int height = std::max(80, 50 + maxPorts * 28); QString caption = captionForNode(data.info); int captionWidth = caption.length() * 8 + 40; int maxInputLen = 0; int maxOutputLen = 0; for (const auto &p : data.inputPorts) maxInputLen = std::max(maxInputLen, static_cast(p.name.length())); for (const auto &p : data.outputPorts) maxOutputLen = std::max(maxOutputLen, static_cast(p.name.length())); int portWidth = (maxInputLen + maxOutputLen) * 7 + 60; int width = std::max(180, std::max(captionWidth, portWidth)); return QSize(width, height); } QPointF WarpGraphModel::nextPosition(const WarpNodeData &data) { QSize size = estimateNodeSize(data); double nodeW = size.width(); double nodeH = size.height(); if (m_nextX + nodeW > kMaxRowWidth && m_nextX > 0) { m_nextX = 0.0; m_nextY += m_rowMaxHeight + kVerticalGap; m_rowMaxHeight = 0.0; } QPointF pos(m_nextX, m_nextY); m_nextX += nodeW + kHorizontalGap; m_rowMaxHeight = std::max(m_rowMaxHeight, nodeH); 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 { QSizeF newSize(estimateNodeSize(data)); constexpr int kMaxAttempts = 50; for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { QRectF newRect(candidate, newSize); bool overlaps = false; for (const auto &[existingId, existingPos] : m_positions) { auto nodeIt = m_nodes.find(existingId); if (nodeIt == m_nodes.end()) continue; QSizeF existingSize; auto sizeIt = m_sizes.find(existingId); if (sizeIt != m_sizes.end()) { existingSize = QSizeF(sizeIt->second); } else { existingSize = QSizeF(estimateNodeSize(nodeIt->second)); } QRectF existingRect(existingPos, existingSize); QRectF padded = existingRect.adjusted(-kHorizontalGap / 2, -kVerticalGap / 2, kHorizontalGap / 2, kVerticalGap / 2); if (newRect.intersects(padded)) { candidate.setY(existingRect.bottom() + kVerticalGap); overlaps = true; break; } } if (!overlaps) break; } return candidate; } 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) { if (data.info.id.value != 0) return data.info.id.value; auto groupIt = m_appGroups.find(qtId); if (groupIt != m_appGroups.end() && !groupIt->second.memberPwIds.empty()) return groupIt->second.memberPwIds.front(); } } return 0; } bool WarpGraphModel::isGroupNode(QtNodes::NodeId nodeId) const { return m_appGroups.find(nodeId) != m_appGroups.end(); } const AppGroupData *WarpGraphModel::appGroupData(QtNodes::NodeId nodeId) const { auto it = m_appGroups.find(nodeId); return it != m_appGroups.end() ? &it->second : nullptr; } WarpNodeType WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) { const std::string &mc = info.media_class; if (mc == "Audio/Sink" || mc == "Audio/Duplex") { return info.is_virtual ? WarpNodeType::kVirtualSink : WarpNodeType::kHardwareSink; } if (mc == "Audio/Source") { return info.is_virtual ? WarpNodeType::kVirtualSource : WarpNodeType::kHardwareSource; } if (mc == "Stream/Output/Audio" || mc == "Stream/Input/Audio") { return WarpNodeType::kApplication; } if (mc == "Video/Source") { return WarpNodeType::kVideoSource; } if (mc == "Video/Sink") { return WarpNodeType::kVideoSink; } return WarpNodeType::kUnknown; } void WarpGraphModel::setNodeVolumeState(QtNodes::NodeId nodeId, const NodeVolumeState &state) { if (!nodeExists(nodeId)) return; NodeVolumeState previous = m_volumeStates[nodeId]; m_volumeStates[nodeId] = state; if (m_client) { auto groupIt = m_appGroups.find(nodeId); if (groupIt != m_appGroups.end()) { for (uint32_t memberPwId : groupIt->second.memberPwIds) { #ifdef WARPPIPE_TESTING m_client->Test_SetNodeVolume(warppipe::NodeId{memberPwId}, state.volume, state.mute); #else m_client->SetNodeVolume(warppipe::NodeId{memberPwId}, state.volume, state.mute); #endif } } else { auto it = m_nodes.find(nodeId); if (it != m_nodes.end() && it->second.info.id.value != 0) { #ifdef WARPPIPE_TESTING m_client->Test_SetNodeVolume(it->second.info.id, state.volume, state.mute); #else m_client->SetNodeVolume(it->second.info.id, state.volume, state.mute); #endif } } } auto wIt = m_volumeWidgets.find(nodeId); 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); } } Q_EMIT nodeVolumeChanged(nodeId, previous, state); } WarpGraphModel::NodeVolumeState WarpGraphModel::nodeVolumeState(QtNodes::NodeId nodeId) const { auto it = m_volumeStates.find(nodeId); if (it != m_volumeStates.end()) return it->second; return {}; } void WarpGraphModel::setNodePeakLevel(QtNodes::NodeId nodeId, float level) { constexpr float kDecay = 0.82f; float &stored = m_peakLevels[nodeId]; stored = std::max(level, stored * kDecay); } float WarpGraphModel::nodePeakLevel(QtNodes::NodeId nodeId) const { auto it = m_peakLevels.find(nodeId); return it != m_peakLevels.end() ? it->second : 0.0f; } void WarpGraphModel::recomputeConnectionChannels() { m_connectionChannels.clear(); std::unordered_map> byTarget; for (const auto &cId : m_connections) byTarget[cId.inNodeId].push_back(cId); for (auto &[targetId, conns] : byTarget) { std::sort(conns.begin(), conns.end(), [this](const auto &a, const auto &b) { auto posA = m_positions.count(a.outNodeId) ? m_positions.at(a.outNodeId).y() : 0.0; auto posB = m_positions.count(b.outNodeId) ? m_positions.at(b.outNodeId).y() : 0.0; if (posA != posB) return posA < posB; return a.outPortIndex < b.outPortIndex; }); int count = static_cast(conns.size()); for (int i = 0; i < count; ++i) m_connectionChannels[conns[i]] = {i, count}; } } WarpGraphModel::ConnectionChannel WarpGraphModel::connectionChannel(QtNodes::ConnectionId cId) const { auto it = m_connectionChannels.find(cId); return it != m_connectionChannels.end() ? it->second : ConnectionChannel{0, 1}; } void WarpGraphModel::saveLayout(const QString &path) const { ViewState vs{}; saveLayout(path, vs); } void WarpGraphModel::saveLayout(const QString &path, const ViewState &viewState) const { QJsonArray nodesArray; for (const auto &[qtId, data] : m_nodes) { auto posIt = m_positions.find(qtId); if (posIt == m_positions.end()) continue; QJsonObject nodeObj; auto groupIt = m_appGroups.find(qtId); if (groupIt != m_appGroups.end()) { nodeObj["name"] = QString::fromStdString("group:" + groupIt->second.groupKey); } else { nodeObj["name"] = QString::fromStdString(data.info.name); } nodeObj["x"] = posIt->second.x(); nodeObj["y"] = posIt->second.y(); nodesArray.append(nodeObj); } QJsonArray ghostsArray; for (const auto &ghostId : m_ghostNodes) { auto nodeIt = m_nodes.find(ghostId); if (nodeIt == m_nodes.end()) continue; const auto &data = nodeIt->second; QJsonObject ghostObj; ghostObj["name"] = QString::fromStdString(data.info.name); auto ghostGroupIt = m_appGroups.find(ghostId); if (ghostGroupIt != m_appGroups.end()) { ghostObj["is_group"] = true; ghostObj["group_key"] = QString::fromStdString(ghostGroupIt->second.groupKey); } ghostObj["description"] = QString::fromStdString(data.info.description); ghostObj["media_class"] = QString::fromStdString(data.info.media_class); ghostObj["application_name"] = QString::fromStdString(data.info.application_name); auto posIt = m_positions.find(ghostId); if (posIt != m_positions.end()) { ghostObj["x"] = posIt->second.x(); ghostObj["y"] = posIt->second.y(); } QJsonArray inPorts; for (const auto &port : data.inputPorts) { QJsonObject p; p["id"] = static_cast(port.id.value); p["name"] = QString::fromStdString(port.name); inPorts.append(p); } ghostObj["input_ports"] = inPorts; QJsonArray outPorts; for (const auto &port : data.outputPorts) { QJsonObject p; p["id"] = static_cast(port.id.value); p["name"] = QString::fromStdString(port.name); outPorts.append(p); } ghostObj["output_ports"] = outPorts; ghostsArray.append(ghostObj); } QJsonArray ghostConnsArray; for (const auto &conn : m_ghostConnections) { auto outIt = m_nodes.find(conn.outNodeId); auto inIt = m_nodes.find(conn.inNodeId); if (outIt == m_nodes.end() || inIt == m_nodes.end()) { continue; } auto outIdx = static_cast(conn.outPortIndex); auto inIdx = static_cast(conn.inPortIndex); if (outIdx >= outIt->second.outputPorts.size() || inIdx >= inIt->second.inputPorts.size()) { continue; } QJsonObject connObj; connObj["out_node"] = QString::fromStdString(outIt->second.info.name); connObj["out_port"] = QString::fromStdString(outIt->second.outputPorts[outIdx].name); connObj["in_node"] = QString::fromStdString(inIt->second.info.name); connObj["in_port"] = QString::fromStdString(inIt->second.inputPorts[inIdx].name); ghostConnsArray.append(connObj); } QJsonObject root; root["version"] = 2; root["nodes"] = nodesArray; root["ghosts"] = ghostsArray; root["ghost_connections"] = ghostConnsArray; if (viewState.valid) { QJsonObject viewObj; viewObj["scale"] = viewState.scale; viewObj["center_x"] = viewState.centerX; viewObj["center_y"] = viewState.centerY; if (viewState.splitterGraph > 0 || viewState.splitterSidebar > 0) { viewObj["splitter_graph"] = viewState.splitterGraph; 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; } QFileInfo fi(path); QDir dir = fi.absoluteDir(); if (!dir.exists()) { dir.mkpath("."); } QFile file(path); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); } } void WarpGraphModel::clearSavedPositions() { m_savedPositions.clear(); m_positions.clear(); } WarpGraphModel::ViewState WarpGraphModel::savedViewState() const { return m_savedViewState; } bool WarpGraphModel::loadLayout(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return false; } QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (!doc.isObject()) { return false; } QJsonObject root = doc.object(); int version = root["version"].toInt(); if (version < 1 || version > 2) { return false; } m_savedPositions.clear(); QJsonArray nodesArray = root["nodes"].toArray(); for (const auto &val : nodesArray) { QJsonObject obj = val.toObject(); std::string name = obj["name"].toString().toStdString(); double x = obj["x"].toDouble(); double y = obj["y"].toDouble(); m_savedPositions[name] = QPointF(x, y); } m_savedViewState = {}; if (root.contains("view")) { QJsonObject viewObj = root["view"].toObject(); m_savedViewState.scale = viewObj["scale"].toDouble(1.0); m_savedViewState.centerX = viewObj["center_x"].toDouble(); m_savedViewState.centerY = viewObj["center_y"].toDouble(); 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; } if (root.contains("ghosts")) { QJsonArray ghostsArray = root["ghosts"].toArray(); for (const auto &val : ghostsArray) { QJsonObject obj = val.toObject(); std::string name = obj["name"].toString().toStdString(); bool alreadyExists = false; for (const auto &[_, data] : m_nodes) { if (data.info.name == name) { alreadyExists = true; break; } } if (alreadyExists) { continue; } warppipe::NodeInfo info; info.id = warppipe::NodeId{0}; info.name = name; info.description = obj["description"].toString().toStdString(); info.media_class = obj["media_class"].toString().toStdString(); info.application_name = obj["application_name"].toString().toStdString(); WarpNodeData data; data.info = info; for (const auto &pval : obj["input_ports"].toArray()) { QJsonObject p = pval.toObject(); warppipe::PortInfo port; port.id = warppipe::PortId{ static_cast(p["id"].toInt())}; port.node = info.id; port.name = p["name"].toString().toStdString(); port.is_input = true; data.inputPorts.push_back(port); } for (const auto &pval : obj["output_ports"].toArray()) { QJsonObject p = pval.toObject(); warppipe::PortInfo port; port.id = warppipe::PortId{ static_cast(p["id"].toInt())}; port.node = info.id; port.name = p["name"].toString().toStdString(); port.is_input = false; data.outputPorts.push_back(port); } QtNodes::NodeId qtId = newNodeId(); m_nodes.emplace(qtId, std::move(data)); m_ghostNodes.insert(qtId); if (obj.value("is_group").toBool()) { std::string groupKey = obj["group_key"].toString().toStdString(); if (groupKey.empty()) groupKey = appGroupKey(info); AppGroupData group; group.groupKey = groupKey; m_appGroups[qtId] = std::move(group); m_groupKeyToQt[groupKey] = qtId; } if (obj.contains("x") && obj.contains("y")) { m_positions.emplace(qtId, QPointF(obj["x"].toDouble(), obj["y"].toDouble())); } std::string posKey = name; auto gIt = m_appGroups.find(qtId); if (gIt != m_appGroups.end()) posKey = "group:" + gIt->second.groupKey; m_savedPositions[posKey] = m_positions.count(qtId) ? m_positions.at(qtId) : QPointF(0, 0); if (nodeHasVolume(classifyNode(info))) { auto *volumeWidget = new NodeVolumeWidget(); m_volumeWidgets[qtId] = volumeWidget; m_volumeStates[qtId] = {}; } Q_EMIT nodeCreated(qtId); } } if (root.contains("ghost_connections")) { m_pendingGhostConnections.clear(); QJsonArray gcArray = root["ghost_connections"].toArray(); for (const auto &val : gcArray) { QJsonObject obj = val.toObject(); PendingGhostConnection pgc; pgc.outNodeName = obj["out_node"].toString().toStdString(); pgc.outPortName = obj["out_port"].toString().toStdString(); pgc.inNodeName = obj["in_node"].toString().toStdString(); pgc.inPortName = obj["in_port"].toString().toStdString(); m_pendingGhostConnections.push_back(std::move(pgc)); } } return !m_savedPositions.empty() || !m_ghostNodes.empty(); } void WarpGraphModel::autoArrange() { struct Column { std::vector ids; double maxWidth = 0.0; }; Column sources; Column apps; Column sinks; Column video; for (const auto &[qtId, data] : m_nodes) { WarpNodeType type = classifyNode(data.info); QSize sz = estimateNodeSize(data); double w = sz.width(); switch (type) { case WarpNodeType::kHardwareSource: case WarpNodeType::kVirtualSource: sources.ids.push_back(qtId); sources.maxWidth = std::max(sources.maxWidth, w); break; case WarpNodeType::kApplication: apps.ids.push_back(qtId); apps.maxWidth = std::max(apps.maxWidth, w); break; case WarpNodeType::kVideoSource: case WarpNodeType::kVideoSink: video.ids.push_back(qtId); video.maxWidth = std::max(video.maxWidth, w); break; default: sinks.ids.push_back(qtId); sinks.maxWidth = std::max(sinks.maxWidth, w); break; } } 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) { auto it = m_nodes.find(id); if (it == m_nodes.end()) continue; QSize sz = estimateNodeSize(it->second); m_positions[id] = QPointF(xOffset, y); Q_EMIT nodePositionUpdated(id); y += sz.height() + kVerticalGap; } }; double x = 0.0; layoutColumn(sources, x); x += sources.maxWidth + kHorizontalGap * 3; layoutColumn(apps, x); x += apps.maxWidth + kHorizontalGap * 3; layoutColumn(sinks, x); if (!video.ids.empty()) { x += sinks.maxWidth + kHorizontalGap * 3; layoutColumn(video, x); } } QVariant WarpGraphModel::styleForNode(WarpNodeType type, bool ghost) { QtNodes::NodeStyle style = QtNodes::StyleCollection::nodeStyle(); QColor base; switch (type) { case WarpNodeType::kHardwareSink: base = QColor(72, 94, 118); break; case WarpNodeType::kHardwareSource: base = QColor(94, 72, 118); break; case WarpNodeType::kVirtualSink: base = QColor(62, 122, 104); break; case WarpNodeType::kVirtualSource: base = QColor(62, 104, 122); break; case WarpNodeType::kApplication: base = QColor(138, 104, 72); break; case WarpNodeType::kVideoSource: base = QColor(120, 80, 130); break; case WarpNodeType::kVideoSink: base = QColor(100, 70, 140); break; default: base = QColor(86, 94, 108); break; } if (ghost) { style.GradientColor0 = base.darker(150); style.GradientColor1 = base.darker(160); style.GradientColor2 = base.darker(170); style.GradientColor3 = base.darker(180); style.NormalBoundaryColor = base.darker(130); style.FontColor = QColor(160, 168, 182); style.FontColorFaded = QColor(120, 128, 142); style.ConnectionPointColor = QColor(140, 148, 160); style.FilledConnectionPointColor = QColor(180, 140, 80); } else { style.GradientColor0 = base.lighter(120); style.GradientColor1 = base.lighter(108); style.GradientColor2 = base.darker(105); style.GradientColor3 = base.darker(120); style.NormalBoundaryColor = base.lighter(135); style.FontColor = QColor(236, 240, 246); style.FontColorFaded = QColor(160, 168, 182); style.ConnectionPointColor = QColor(200, 208, 220); style.FilledConnectionPointColor = QColor(255, 165, 0); } style.Opacity = 1.0f; style.SelectedBoundaryColor = QColor(255, 165, 0); style.PenWidth = 1.3f; style.HoveredPenWidth = 2.4f; style.ConnectionPointDiameter = 10.0f; return style.toJson().toVariantMap(); }