1940 lines
60 KiB
C++
1940 lines
60 KiB
C++
#include "WarpGraphModel.h"
|
|
#include "VolumeWidgets.h"
|
|
|
|
#include <QColor>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QVariant>
|
|
|
|
#include <QtNodes/NodeStyle>
|
|
#include <QtNodes/StyleCollection>
|
|
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
inline int volumeToSlider(float volume) {
|
|
return static_cast<int>(std::round(std::cbrt(volume) * 100.0f));
|
|
}
|
|
}
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
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<QtNodes::NodeId> WarpGraphModel::allNodeIds() const {
|
|
std::unordered_set<QtNodes::NodeId> ids;
|
|
ids.reserve(m_nodes.size());
|
|
for (const auto &entry : m_nodes) {
|
|
ids.insert(entry.first);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
std::unordered_set<QtNodes::ConnectionId>
|
|
WarpGraphModel::allConnectionIds(QtNodes::NodeId const nodeId) const {
|
|
std::unordered_set<QtNodes::ConnectionId> result;
|
|
for (const auto &conn : m_connections) {
|
|
if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) {
|
|
result.insert(conn);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::unordered_set<QtNodes::ConnectionId>
|
|
WarpGraphModel::connections(QtNodes::NodeId nodeId,
|
|
QtNodes::PortType portType,
|
|
QtNodes::PortIndex portIndex) const {
|
|
std::unordered_set<QtNodes::ConnectionId> 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<size_t>(connectionId.outPortIndex);
|
|
auto inIdx = static_cast<size_t>(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<warppipe::PortId> outPorts;
|
|
std::vector<warppipe::PortId> inPorts;
|
|
|
|
if (outGroupIt != m_appGroups.end()) {
|
|
auto mapIt = outGroupIt->second.outputPortMap.find(
|
|
static_cast<unsigned>(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<size_t>(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<unsigned>(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<size_t>(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<int>(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<unsigned int>(data.inputPorts.size());
|
|
case QtNodes::NodeRole::OutPortCount:
|
|
return static_cast<unsigned int>(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<QWidget *>(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<size_t>(portIndex);
|
|
if (idx < data.inputPorts.size()) {
|
|
return QString::fromStdString(data.inputPorts[idx].name);
|
|
}
|
|
} else if (portType == QtNodes::PortType::Out) {
|
|
auto idx = static_cast<size_t>(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<uint32_t> 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);
|
|
Q_EMIT connectionDeleted(connectionId);
|
|
return true;
|
|
}
|
|
|
|
bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) {
|
|
if (!nodeExists(nodeId)) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<QtNodes::ConnectionId> 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_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<qint64>(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<unsigned>(ci)].push_back(port.id);
|
|
m_portToGroupPort[port.id.value] = {groupQtId,
|
|
static_cast<QtNodes::PortIndex>(ci),
|
|
false};
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (size_t ci = 0; ci < canonIn.size(); ++ci) {
|
|
if (port.name == canonIn[ci].name) {
|
|
group.inputPortMap[static_cast<unsigned>(ci)].push_back(port.id);
|
|
m_portToGroupPort[port.id.value] = {groupQtId,
|
|
static_cast<QtNodes::PortIndex>(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<uint32_t> seenPwIds;
|
|
|
|
// Phase 1: Separate app streams (to be grouped) from other nodes.
|
|
std::unordered_map<std::string, std::vector<warppipe::NodeInfo>> appStreams;
|
|
std::vector<warppipe::NodeInfo> 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<warppipe::PortInfo> inputs;
|
|
std::vector<warppipe::PortInfo> 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<std::string> 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<uint32_t> 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<uint32_t> 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<std::string> 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<uint32_t> 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<QtNodes::PortIndex>(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<QtNodes::PortIndex>(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<uint32_t> 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<QtNodes::PortIndex>(i);
|
|
break;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < inNodeIt->second.inputPorts.size(); ++i) {
|
|
if (inNodeIt->second.inputPorts[i].name == it->inPortName) {
|
|
inIdx = static_cast<QtNodes::PortIndex>(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<NodeVolumeWidget *>(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<NodeVolumeWidget *>(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<int>(
|
|
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<int>(p.name.length()));
|
|
for (const auto &p : data.outputPorts)
|
|
maxOutputLen = std::max(maxOutputLen, static_cast<int>(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<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));
|
|
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<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) {
|
|
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<NodeVolumeWidget *>(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<QtNodes::NodeId, std::vector<QtNodes::ConnectionId>> 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<int>(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<int>(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<int>(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<size_t>(conn.outPortIndex);
|
|
auto inIdx = static_cast<size_t>(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<uint32_t>(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<uint32_t>(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<QtNodes::NodeId> 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();
|
|
}
|