GUI Milestone 2
This commit is contained in:
parent
f46f9542b4
commit
8f341f631a
3 changed files with 190 additions and 22 deletions
42
GUI_PLAN.md
42
GUI_PLAN.md
|
|
@ -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] Use port name from PortInfo as port label
|
||||||
- [x] Verify nodes appear in graph view with correct titles and ports
|
- [x] Verify nodes appear in graph view with correct titles and ports
|
||||||
|
|
||||||
- [ ] Milestone 2 - Visual Styling and Node Types
|
- [x] Milestone 2 - Visual Styling and Node Types
|
||||||
- [ ] Define node type classification based on `media_class`:
|
- [x] Define node type classification based on `media_class`:
|
||||||
- [ ] Sink → "Hardware Sink" (blue-gray base color)
|
- [x] Sink → "Hardware Sink" (blue-gray base color)
|
||||||
- [ ] Source → "Hardware Source" (blue-gray base color)
|
- [x] Source → "Hardware Source" (blue-gray base color)
|
||||||
- [ ] Virtual sinks created by warppipe → "Virtual Sink" (green base color)
|
- [x] Virtual sinks created by warppipe → "Virtual Sink" (green base color)
|
||||||
- [ ] Virtual sources created by warppipe → "Virtual Source" (green base color)
|
- [x] Virtual sources created by warppipe → "Virtual Source" (green base color)
|
||||||
- [ ] Application audio streams (ephemeral) → "Application" (brown/orange base color)
|
- [x] Application audio streams (ephemeral) → "Application" (brown/orange base color)
|
||||||
- [ ] Implement custom NodeStyle via `nodeData(NodeRole::Style)`:
|
- [x] Implement custom NodeStyle via `nodeData(NodeRole::Style)`:
|
||||||
- [ ] Return QtNodes::NodeStyle::toJson().toVariantMap()
|
- [x] Return QtNodes::NodeStyle::toJson().toVariantMap()
|
||||||
- [ ] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type
|
- [x] Set GradientColor0-3, NormalBoundaryColor, FontColor based on node type
|
||||||
- [ ] Reference potato's `nodeStyleVariant()` function for color scheme
|
- [x] Reference potato's `nodeStyleVariant()` function for color scheme
|
||||||
- [ ] Detect ephemeral (application) nodes:
|
- [x] Detect ephemeral (application) nodes:
|
||||||
- [ ] Track node appearance/disappearance via Client poll or registry events
|
- [x] Track node appearance/disappearance via Client poll or registry events
|
||||||
- [ ] Mark node as "inactive" if it disappears (no audio playing)
|
- [x] Mark node as "inactive" if it disappears (no audio playing)
|
||||||
- [ ] Persist inactive nodes in graph model (do NOT remove from visual graph)
|
- [x] Persist inactive nodes in graph model (do NOT remove from visual graph)
|
||||||
- [ ] Apply "ghost" styling to inactive nodes:
|
- [x] Apply "ghost" styling to inactive nodes:
|
||||||
- [ ] Set `Opacity = 0.6f` (vs 1.0f for active)
|
- [x] Set `Opacity = 0.6f` (vs 1.0f for active)
|
||||||
- [ ] Darken gradient colors (use `.darker(150-180)`)
|
- [x] Darken gradient colors (use `.darker(150-180)`)
|
||||||
- [ ] Fade font color (lighter gray)
|
- [x] Fade font color (lighter gray)
|
||||||
- [ ] Keep connections visible with faded style
|
- [x] Keep connections visible with faded style
|
||||||
- [ ] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear
|
- [x] Verify: Application nodes appear vibrant when active, fade when inactive, never disappear
|
||||||
|
|
||||||
- [ ] Milestone 3 - Link Visualization and Drag-Connect
|
- [ ] Milestone 3 - Link Visualization and Drag-Connect
|
||||||
- [ ] Implement connection mapping:
|
- [ ] Implement connection mapping:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
#include "WarpGraphModel.h"
|
#include "WarpGraphModel.h"
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
|
#include <QtNodes/NodeStyle>
|
||||||
|
#include <QtNodes/StyleCollection>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
|
@ -141,6 +145,11 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId,
|
||||||
return static_cast<unsigned int>(data.outputPorts.size());
|
return static_cast<unsigned int>(data.outputPorts.size());
|
||||||
case QtNodes::NodeRole::Type:
|
case QtNodes::NodeRole::Type:
|
||||||
return QString("PipeWire");
|
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:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
@ -275,8 +284,50 @@ void WarpGraphModel::refreshFromClient() {
|
||||||
|
|
||||||
auto existing = m_pwToQt.find(nodeInfo.id.value);
|
auto existing = m_pwToQt.find(nodeInfo.id.value);
|
||||||
if (existing != m_pwToQt.end()) {
|
if (existing != m_pwToQt.end()) {
|
||||||
auto &data = m_nodes[existing->second];
|
QtNodes::NodeId qtId = existing->second;
|
||||||
|
auto &data = m_nodes[qtId];
|
||||||
data.info = nodeInfo;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,6 +361,33 @@ void WarpGraphModel::refreshFromClient() {
|
||||||
Q_EMIT nodeCreated(qtId);
|
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();
|
auto linksResult = m_client->ListLinks();
|
||||||
if (linksResult.ok()) {
|
if (linksResult.ok()) {
|
||||||
std::unordered_set<uint32_t> seenLinkIds;
|
std::unordered_set<uint32_t> seenLinkIds;
|
||||||
|
|
@ -407,3 +485,80 @@ QPointF WarpGraphModel::nextPosition() const {
|
||||||
double y = (count / 4) * 200.0;
|
double y = (count / 4) * 200.0;
|
||||||
return QPointF(x, y);
|
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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,15 @@
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
enum class WarpNodeType : uint8_t {
|
||||||
|
kUnknown = 0,
|
||||||
|
kHardwareSink,
|
||||||
|
kHardwareSource,
|
||||||
|
kVirtualSink,
|
||||||
|
kVirtualSource,
|
||||||
|
kApplication,
|
||||||
|
};
|
||||||
|
|
||||||
struct WarpNodeData {
|
struct WarpNodeData {
|
||||||
warppipe::NodeInfo info;
|
warppipe::NodeInfo info;
|
||||||
std::vector<warppipe::PortInfo> inputPorts;
|
std::vector<warppipe::PortInfo> inputPorts;
|
||||||
|
|
@ -54,9 +63,12 @@ public:
|
||||||
void refreshFromClient();
|
void refreshFromClient();
|
||||||
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
|
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
|
||||||
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
|
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
|
||||||
|
bool isGhost(QtNodes::NodeId nodeId) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QString captionForNode(const warppipe::NodeInfo &info);
|
static QString captionForNode(const warppipe::NodeInfo &info);
|
||||||
|
static WarpNodeType classifyNode(const warppipe::NodeInfo &info);
|
||||||
|
static QVariant styleForNode(WarpNodeType type, bool ghost);
|
||||||
QPointF nextPosition() const;
|
QPointF nextPosition() const;
|
||||||
|
|
||||||
warppipe::Client *m_client = nullptr;
|
warppipe::Client *m_client = nullptr;
|
||||||
|
|
@ -70,4 +82,5 @@ private:
|
||||||
|
|
||||||
std::unordered_map<QtNodes::NodeId, QPointF> m_positions;
|
std::unordered_map<QtNodes::NodeId, QPointF> m_positions;
|
||||||
std::unordered_map<QtNodes::NodeId, QSize> m_sizes;
|
std::unordered_map<QtNodes::NodeId, QSize> m_sizes;
|
||||||
|
std::unordered_set<QtNodes::NodeId> m_ghostNodes;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue