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] 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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue