GUI Milestone 2

This commit is contained in:
Joey Yakimowich-Payne 2026-01-29 22:17:33 -07:00
commit 8f341f631a
3 changed files with 190 additions and 22 deletions

View file

@ -35,27 +35,27 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
- [x] Use port name from PortInfo as port label
- [x] Verify nodes appear in graph view with correct titles and ports
- [ ] Milestone 2 - Visual Styling and Node Types
- [ ] Define node type classification based on `media_class`:
- [ ] Sink → "Hardware Sink" (blue-gray base color)
- [ ] Source → "Hardware Source" (blue-gray base color)
- [ ] Virtual sinks created by warppipe → "Virtual Sink" (green base color)
- [ ] Virtual sources created by warppipe → "Virtual Source" (green base color)
- [ ] Application audio streams (ephemeral) → "Application" (brown/orange base color)
- [ ] Implement custom NodeStyle via `nodeData(NodeRole::Style)`:
- [ ] Return QtNodes::NodeStyle::toJson().toVariantMap()
- [ ] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type
- [ ] Reference potato's `nodeStyleVariant()` function for color scheme
- [ ] Detect ephemeral (application) nodes:
- [ ] Track node appearance/disappearance via Client poll or registry events
- [ ] Mark node as "inactive" if it disappears (no audio playing)
- [ ] Persist inactive nodes in graph model (do NOT remove from visual graph)
- [ ] Apply "ghost" styling to inactive nodes:
- [ ] Set `Opacity = 0.6f` (vs 1.0f for active)
- [ ] Darken gradient colors (use `.darker(150-180)`)
- [ ] Fade font color (lighter gray)
- [ ] Keep connections visible with faded style
- [ ] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear
- [x] Milestone 2 - Visual Styling and Node Types
- [x] Define node type classification based on `media_class`:
- [x] Sink → "Hardware Sink" (blue-gray base color)
- [x] Source → "Hardware Source" (blue-gray base color)
- [x] Virtual sinks created by warppipe → "Virtual Sink" (green base color)
- [x] Virtual sources created by warppipe → "Virtual Source" (green base color)
- [x] Application audio streams (ephemeral) → "Application" (brown/orange base color)
- [x] Implement custom NodeStyle via `nodeData(NodeRole::Style)`:
- [x] Return QtNodes::NodeStyle::toJson().toVariantMap()
- [x] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type
- [x] Reference potato's `nodeStyleVariant()` function for color scheme
- [x] Detect ephemeral (application) nodes:
- [x] Track node appearance/disappearance via Client poll or registry events
- [x] Mark node as "inactive" if it disappears (no audio playing)
- [x] Persist inactive nodes in graph model (do NOT remove from visual graph)
- [x] Apply "ghost" styling to inactive nodes:
- [x] Set `Opacity = 0.6f` (vs 1.0f for active)
- [x] Darken gradient colors (use `.darker(150-180)`)
- [x] Fade font color (lighter gray)
- [x] Keep connections visible with faded style
- [x] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear
- [ ] Milestone 3 - Link Visualization and Drag-Connect
- [ ] Implement connection mapping:

View file

@ -1,8 +1,12 @@
#include "WarpGraphModel.h"
#include <QColor>
#include <QJsonObject>
#include <QVariant>
#include <QtNodes/NodeStyle>
#include <QtNodes/StyleCollection>
#include <algorithm>
#include <cmath>
@ -141,6 +145,11 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId,
return static_cast<unsigned int>(data.outputPorts.size());
case QtNodes::NodeRole::Type:
return QString("PipeWire");
case QtNodes::NodeRole::Style: {
bool ghost = m_ghostNodes.find(nodeId) != m_ghostNodes.end();
WarpNodeType type = classifyNode(data.info);
return styleForNode(type, ghost);
}
default:
return QVariant();
}
@ -275,8 +284,50 @@ void WarpGraphModel::refreshFromClient() {
auto existing = m_pwToQt.find(nodeInfo.id.value);
if (existing != m_pwToQt.end()) {
auto &data = m_nodes[existing->second];
QtNodes::NodeId qtId = existing->second;
auto &data = m_nodes[qtId];
data.info = nodeInfo;
if (m_ghostNodes.erase(qtId)) {
Q_EMIT nodeUpdated(qtId);
}
continue;
}
QtNodes::NodeId ghostMatch = 0;
std::string nodeName = nodeInfo.name;
for (const auto &ghostId : m_ghostNodes) {
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);
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;
}
@ -310,6 +361,33 @@ void WarpGraphModel::refreshFromClient() {
Q_EMIT nodeCreated(qtId);
}
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;
}
QtNodes::NodeId qtId = it->second;
auto nodeIt = m_nodes.find(qtId);
if (nodeIt == m_nodes.end()) {
continue;
}
WarpNodeType type = classifyNode(nodeIt->second.info);
if (type == WarpNodeType::kApplication) {
m_ghostNodes.insert(qtId);
m_pwToQt.erase(it);
Q_EMIT nodeUpdated(qtId);
} else {
m_pwToQt.erase(it);
deleteNode(qtId);
}
}
auto linksResult = m_client->ListLinks();
if (linksResult.ok()) {
std::unordered_set<uint32_t> seenLinkIds;
@ -407,3 +485,80 @@ QPointF WarpGraphModel::nextPosition() const {
double y = (count / 4) * 200.0;
return QPointF(x, y);
}
bool WarpGraphModel::isGhost(QtNodes::NodeId nodeId) const {
return m_ghostNodes.find(nodeId) != m_ghostNodes.end();
}
WarpNodeType
WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) {
const std::string &mc = info.media_class;
bool isVirtual = (info.name.find("warppipe") != std::string::npos);
if (mc == "Audio/Sink" || mc == "Audio/Duplex") {
return isVirtual ? WarpNodeType::kVirtualSink : WarpNodeType::kHardwareSink;
}
if (mc == "Audio/Source") {
return isVirtual ? WarpNodeType::kVirtualSource
: WarpNodeType::kHardwareSource;
}
if (mc == "Stream/Output/Audio" || mc == "Stream/Input/Audio") {
return WarpNodeType::kApplication;
}
return WarpNodeType::kUnknown;
}
QVariant WarpGraphModel::styleForNode(WarpNodeType type, bool ghost) {
QtNodes::NodeStyle style = QtNodes::StyleCollection::nodeStyle();
QColor base;
switch (type) {
case WarpNodeType::kHardwareSink:
case WarpNodeType::kHardwareSource:
base = QColor(72, 94, 118);
break;
case WarpNodeType::kVirtualSink:
case WarpNodeType::kVirtualSource:
base = QColor(62, 122, 104);
break;
case WarpNodeType::kApplication:
base = QColor(138, 104, 72);
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);
style.Opacity = 0.6f;
} 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();
}

View file

@ -13,6 +13,15 @@
#include <unordered_map>
#include <unordered_set>
enum class WarpNodeType : uint8_t {
kUnknown = 0,
kHardwareSink,
kHardwareSource,
kVirtualSink,
kVirtualSource,
kApplication,
};
struct WarpNodeData {
warppipe::NodeInfo info;
std::vector<warppipe::PortInfo> inputPorts;
@ -54,9 +63,12 @@ public:
void refreshFromClient();
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
bool isGhost(QtNodes::NodeId nodeId) const;
private:
static QString captionForNode(const warppipe::NodeInfo &info);
static WarpNodeType classifyNode(const warppipe::NodeInfo &info);
static QVariant styleForNode(WarpNodeType type, bool ghost);
QPointF nextPosition() const;
warppipe::Client *m_client = nullptr;
@ -70,4 +82,5 @@ private:
std::unordered_map<QtNodes::NodeId, QPointF> m_positions;
std::unordered_map<QtNodes::NodeId, QSize> m_sizes;
std::unordered_set<QtNodes::NodeId> m_ghostNodes;
};