GUI Milestone 4
This commit is contained in:
parent
282136632e
commit
a369381b6c
5 changed files with 177 additions and 16 deletions
28
GUI_PLAN.md
28
GUI_PLAN.md
|
|
@ -74,20 +74,20 @@ A Qt6-based node editor GUI for warppipe using the QtNodes (nodeeditor) library.
|
|||
- [x] Remove from m_connections
|
||||
- [x] Verify: Drag connection from output port to input port creates PipeWire link; delete removes it
|
||||
|
||||
- [ ] Milestone 4 - Context Menu and Virtual Node Creation
|
||||
- [ ] Add context menu to GraphEditorWidget:
|
||||
- [ ] Right-click on canvas (not on node) shows menu
|
||||
- [ ] Menu items: "Create Virtual Sink", "Create Virtual Source"
|
||||
- [ ] Implement "Create Virtual Sink":
|
||||
- [ ] Prompt user for name (QInputDialog or inline text field)
|
||||
- [ ] Call `Client::CreateVirtualSink(name, VirtualNodeOptions{})` with default options
|
||||
- [ ] On success, node appears in graph at context menu position
|
||||
- [ ] Implement "Create Virtual Source":
|
||||
- [ ] Same as sink but call `Client::CreateVirtualSource()`
|
||||
- [ ] Add context menu on nodes:
|
||||
- [ ] Right-click on virtual node shows "Delete Node" option
|
||||
- [ ] Call `Client::RemoveNode(nodeId)` and remove from graph
|
||||
- [ ] Verify: Can create/delete virtual sinks and sources via right-click
|
||||
- [x] Milestone 4 - Context Menu and Virtual Node Creation
|
||||
- [x] Add context menu to GraphEditorWidget:
|
||||
- [x] Right-click on canvas (not on node) shows menu
|
||||
- [x] Menu items: "Create Virtual Sink", "Create Virtual Source"
|
||||
- [x] Implement "Create Virtual Sink":
|
||||
- [x] Prompt user for name (QInputDialog or inline text field)
|
||||
- [x] Call `Client::CreateVirtualSink(name, VirtualNodeOptions{})` with default options
|
||||
- [x] On success, node appears in graph at context menu position
|
||||
- [x] Implement "Create Virtual Source":
|
||||
- [x] Same as sink but call `Client::CreateVirtualSource()`
|
||||
- [x] Add context menu on nodes:
|
||||
- [x] Right-click on virtual node shows "Delete Node" option
|
||||
- [x] Call `Client::RemoveNode(nodeId)` and remove from graph
|
||||
- [x] Verify: Can create/delete virtual sinks and sources via right-click
|
||||
|
||||
- [ ] Milestone 5 - Layout Persistence and Polish
|
||||
- [ ] Implement layout save/load:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
#include <QtNodes/BasicGraphicsScene>
|
||||
#include <QtNodes/GraphicsView>
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
|
|
@ -18,6 +21,10 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(m_view);
|
||||
|
||||
m_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_view, &QWidget::customContextMenuRequested, this,
|
||||
&GraphEditorWidget::onContextMenuRequested);
|
||||
|
||||
m_model->refreshFromClient();
|
||||
|
||||
m_refreshTimer = new QTimer(this);
|
||||
|
|
@ -27,3 +34,105 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
}
|
||||
|
||||
void GraphEditorWidget::onRefreshTimer() { m_model->refreshFromClient(); }
|
||||
|
||||
void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
|
||||
QPointF scenePos = m_view->mapToScene(pos);
|
||||
|
||||
uint32_t hitPwNodeId = 0;
|
||||
for (auto nodeId : m_model->allNodeIds()) {
|
||||
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
QPointF nodePos =
|
||||
m_model->nodeData(nodeId, QtNodes::NodeRole::Position).toPointF();
|
||||
QSize nodeSize =
|
||||
m_model->nodeData(nodeId, QtNodes::NodeRole::Size).toSize();
|
||||
QRectF nodeRect(nodePos, QSizeF(nodeSize));
|
||||
if (nodeRect.contains(scenePos)) {
|
||||
hitPwNodeId = data->info.id.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QPoint screenPos = m_view->mapToGlobal(pos);
|
||||
if (hitPwNodeId != 0) {
|
||||
showNodeContextMenu(screenPos, hitPwNodeId);
|
||||
} else {
|
||||
showCanvasContextMenu(screenPos, scenePos);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos,
|
||||
const QPointF &scenePos) {
|
||||
QMenu menu;
|
||||
QAction *createSink = menu.addAction(QStringLiteral("Create Virtual Sink"));
|
||||
QAction *createSource =
|
||||
menu.addAction(QStringLiteral("Create Virtual Source"));
|
||||
|
||||
QAction *chosen = menu.exec(screenPos);
|
||||
if (chosen == createSink) {
|
||||
createVirtualNode(true, scenePos);
|
||||
} else if (chosen == createSource) {
|
||||
createVirtualNode(false, scenePos);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||
uint32_t pwNodeId) {
|
||||
QtNodes::NodeId qtId = m_model->qtNodeIdForPw(pwNodeId);
|
||||
const WarpNodeData *data = m_model->warpNodeData(qtId);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
WarpNodeType type = WarpGraphModel::classifyNode(data->info);
|
||||
bool isVirtual =
|
||||
type == WarpNodeType::kVirtualSink || type == WarpNodeType::kVirtualSource;
|
||||
|
||||
if (!isVirtual) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMenu menu;
|
||||
QAction *deleteAction = menu.addAction(QStringLiteral("Delete Node"));
|
||||
|
||||
QAction *chosen = menu.exec(screenPos);
|
||||
if (chosen == deleteAction && m_client) {
|
||||
m_client->RemoveNode(warppipe::NodeId{pwNodeId});
|
||||
m_model->refreshFromClient();
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEditorWidget::createVirtualNode(bool isSink,
|
||||
const QPointF &scenePos) {
|
||||
QString label = isSink ? QStringLiteral("Create Virtual Sink")
|
||||
: QStringLiteral("Create Virtual Source");
|
||||
bool ok = false;
|
||||
QString name = QInputDialog::getText(this, label,
|
||||
QStringLiteral("Node name:"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok || name.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string nodeName = name.trimmed().toStdString();
|
||||
m_model->setPendingPosition(nodeName, scenePos);
|
||||
|
||||
warppipe::Status status;
|
||||
if (isSink) {
|
||||
auto result = m_client->CreateVirtualSink(nodeName);
|
||||
status = result.status;
|
||||
} else {
|
||||
auto result = m_client->CreateVirtualSource(nodeName);
|
||||
status = result.status;
|
||||
}
|
||||
|
||||
if (!status.ok()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||
QString::fromStdString(status.message));
|
||||
return;
|
||||
}
|
||||
|
||||
m_model->refreshFromClient();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,13 @@ public:
|
|||
|
||||
private slots:
|
||||
void onRefreshTimer();
|
||||
void onContextMenuRequested(const QPoint &pos);
|
||||
|
||||
private:
|
||||
void showCanvasContextMenu(const QPoint &screenPos, const QPointF &scenePos);
|
||||
void showNodeContextMenu(const QPoint &screenPos, uint32_t pwNodeId);
|
||||
void createVirtualNode(bool isSink, const QPointF &scenePos);
|
||||
|
||||
warppipe::Client *m_client = nullptr;
|
||||
WarpGraphModel *m_model = nullptr;
|
||||
QtNodes::BasicGraphicsScene *m_scene = nullptr;
|
||||
|
|
|
|||
|
|
@ -318,11 +318,43 @@ void WarpGraphModel::refreshFromClient() {
|
|||
for (const auto &nodeInfo : nodesResult.value) {
|
||||
seenPwIds.insert(nodeInfo.id.value);
|
||||
|
||||
WarpNodeType nodeType = classifyNode(nodeInfo);
|
||||
bool isStream = nodeType == WarpNodeType::kApplication;
|
||||
if (isStream && nodeInfo.name.empty() &&
|
||||
nodeInfo.application_name.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto existing = m_pwToQt.find(nodeInfo.id.value);
|
||||
if (existing != m_pwToQt.end()) {
|
||||
QtNodes::NodeId qtId = existing->second;
|
||||
auto &data = m_nodes[qtId];
|
||||
data.info = nodeInfo;
|
||||
|
||||
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)) {
|
||||
Q_EMIT nodeUpdated(qtId);
|
||||
}
|
||||
|
|
@ -392,7 +424,14 @@ void WarpGraphModel::refreshFromClient() {
|
|||
|
||||
auto [nodeIt, _] = m_nodes.emplace(qtId, std::move(data));
|
||||
m_pwToQt.emplace(nodeInfo.id.value, qtId);
|
||||
m_positions.emplace(qtId, nextPosition(nodeIt->second));
|
||||
|
||||
auto pendingIt = m_pendingPositions.find(nodeInfo.name);
|
||||
if (pendingIt != m_pendingPositions.end()) {
|
||||
m_positions.emplace(qtId, pendingIt->second);
|
||||
m_pendingPositions.erase(pendingIt);
|
||||
} else {
|
||||
m_positions.emplace(qtId, nextPosition(nodeIt->second));
|
||||
}
|
||||
|
||||
Q_EMIT nodeCreated(qtId);
|
||||
}
|
||||
|
|
@ -510,6 +549,11 @@ QtNodes::NodeId WarpGraphModel::qtNodeIdForPw(uint32_t pwNodeId) const {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ public:
|
|||
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
|
||||
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
|
||||
bool isGhost(QtNodes::NodeId nodeId) const;
|
||||
void setPendingPosition(const std::string &nodeName, QPointF pos);
|
||||
static WarpNodeType classifyNode(const warppipe::NodeInfo &info);
|
||||
|
||||
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 WarpNodeData &data);
|
||||
static QSize estimateNodeSize(const WarpNodeData &data);
|
||||
|
|
@ -93,4 +94,6 @@ private:
|
|||
double m_rowMaxHeight = 0.0;
|
||||
|
||||
bool m_refreshing = false;
|
||||
|
||||
std::unordered_map<std::string, QPointF> m_pendingPositions;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue