Node editor
This commit is contained in:
parent
d389161f4a
commit
21cd3bd3f9
4 changed files with 479 additions and 18 deletions
|
|
@ -15,6 +15,7 @@
|
||||||
#include <QtNodes/internal/UndoCommands.hpp>
|
#include <QtNodes/internal/UndoCommands.hpp>
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QCheckBox>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QContextMenuEvent>
|
#include <QContextMenuEvent>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
@ -420,6 +421,25 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
||||||
m_rulesScroll->setWidget(m_rulesContainer);
|
m_rulesScroll->setWidget(m_rulesContainer);
|
||||||
m_sidebar->addTab(m_rulesScroll, QStringLiteral("RULES"));
|
m_sidebar->addTab(m_rulesScroll, QStringLiteral("RULES"));
|
||||||
|
|
||||||
|
m_nodeDetailsScroll = new QScrollArea();
|
||||||
|
m_nodeDetailsScroll->setWidgetResizable(true);
|
||||||
|
m_nodeDetailsScroll->setStyleSheet(m_mixerScroll->styleSheet());
|
||||||
|
m_nodeDetailsContainer = new QWidget();
|
||||||
|
m_nodeDetailsContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;"));
|
||||||
|
auto *nodeDetailsLayout = new QVBoxLayout(m_nodeDetailsContainer);
|
||||||
|
nodeDetailsLayout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
nodeDetailsLayout->setSpacing(6);
|
||||||
|
auto *noSelectionLabel = new QLabel(QStringLiteral("Select a node to view details"));
|
||||||
|
noSelectionLabel->setStyleSheet(QStringLiteral(
|
||||||
|
"color: #6a6a7a; font-style: italic; background: transparent;"));
|
||||||
|
noSelectionLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
noSelectionLabel->setWordWrap(true);
|
||||||
|
nodeDetailsLayout->addStretch();
|
||||||
|
nodeDetailsLayout->addWidget(noSelectionLabel);
|
||||||
|
nodeDetailsLayout->addStretch();
|
||||||
|
m_nodeDetailsScroll->setWidget(m_nodeDetailsContainer);
|
||||||
|
m_sidebar->addTab(m_nodeDetailsScroll, QStringLiteral("NODE"));
|
||||||
|
|
||||||
m_splitter = new QSplitter(Qt::Horizontal);
|
m_splitter = new QSplitter(Qt::Horizontal);
|
||||||
m_splitter->addWidget(m_view);
|
m_splitter->addWidget(m_view);
|
||||||
m_splitter->addWidget(m_sidebar);
|
m_splitter->addWidget(m_sidebar);
|
||||||
|
|
@ -549,8 +569,15 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
||||||
rebuildMixerStrips();
|
rebuildMixerStrips();
|
||||||
rebuildNodeMeters();
|
rebuildNodeMeters();
|
||||||
rebuildRulesList();
|
rebuildRulesList();
|
||||||
|
if (nodeId == m_selectedNodeId) {
|
||||||
|
m_selectedNodeId = 0;
|
||||||
|
clearNodeDetailsPanel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_scene, &QGraphicsScene::selectionChanged, this,
|
||||||
|
&GraphEditorWidget::onSelectionChanged);
|
||||||
|
|
||||||
m_saveTimer = new QTimer(this);
|
m_saveTimer = new QTimer(this);
|
||||||
m_saveTimer->setSingleShot(true);
|
m_saveTimer->setSingleShot(true);
|
||||||
m_saveTimer->setInterval(1000);
|
m_saveTimer->setInterval(1000);
|
||||||
|
|
@ -884,34 +911,113 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||||
|
|
||||||
void GraphEditorWidget::createVirtualNode(bool isSink,
|
void GraphEditorWidget::createVirtualNode(bool isSink,
|
||||||
const QPointF &scenePos) {
|
const QPointF &scenePos) {
|
||||||
QString label = isSink ? QStringLiteral("Create Virtual Sink")
|
if (isSink) {
|
||||||
: QStringLiteral("Create Virtual Source");
|
bool ok = false;
|
||||||
bool ok = false;
|
QString name = QInputDialog::getText(
|
||||||
QString name = QInputDialog::getText(this, label,
|
this, QStringLiteral("Create Virtual Sink"),
|
||||||
QStringLiteral("Node name:"),
|
QStringLiteral("Node name:"), QLineEdit::Normal, QString(), &ok);
|
||||||
QLineEdit::Normal, QString(), &ok);
|
if (!ok || name.trimmed().isEmpty())
|
||||||
if (!ok || name.trimmed().isEmpty()) {
|
return;
|
||||||
|
|
||||||
|
std::string nodeName = name.trimmed().toStdString();
|
||||||
|
m_model->setPendingPosition(nodeName, scenePos);
|
||||||
|
auto result = m_client->CreateVirtualSink(nodeName);
|
||||||
|
if (!result.status.ok()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QString::fromStdString(result.status.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_model->refreshFromClient();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string nodeName = name.trimmed().toStdString();
|
static const QString kDialogStyle = QStringLiteral(
|
||||||
|
"QDialog { background: #1e1e22; }"
|
||||||
|
"QLabel { color: #ecf0f6; }"
|
||||||
|
"QLineEdit { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;"
|
||||||
|
" border-radius: 4px; padding: 4px 8px; }"
|
||||||
|
"QCheckBox { color: #ecf0f6; }"
|
||||||
|
"QCheckBox::indicator { width: 16px; height: 16px; }"
|
||||||
|
"QComboBox { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;"
|
||||||
|
" border-radius: 4px; padding: 4px 8px; }"
|
||||||
|
"QComboBox::drop-down { border: none; }"
|
||||||
|
"QComboBox QAbstractItemView { background: #2a2a32; color: #ecf0f6;"
|
||||||
|
" selection-background-color: #3a3a44; }");
|
||||||
|
|
||||||
|
static const QString kButtonStyle = QStringLiteral(
|
||||||
|
"QPushButton { background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;"
|
||||||
|
" border-radius: 4px; padding: 6px 16px; }"
|
||||||
|
"QPushButton:hover { background: #3a3a44; }");
|
||||||
|
|
||||||
|
QDialog dlg(this);
|
||||||
|
dlg.setWindowTitle(QStringLiteral("Create Virtual Source"));
|
||||||
|
dlg.setStyleSheet(kDialogStyle);
|
||||||
|
|
||||||
|
auto *form = new QFormLayout(&dlg);
|
||||||
|
form->setContentsMargins(16, 16, 16, 16);
|
||||||
|
form->setSpacing(8);
|
||||||
|
|
||||||
|
auto *nameEdit = new QLineEdit();
|
||||||
|
nameEdit->setPlaceholderText(QStringLiteral("e.g. Desktop Audio"));
|
||||||
|
form->addRow(QStringLiteral("Name:"), nameEdit);
|
||||||
|
|
||||||
|
auto *loopbackCheck = new QCheckBox(QStringLiteral("Loopback from another node"));
|
||||||
|
form->addRow(loopbackCheck);
|
||||||
|
|
||||||
|
auto *targetCombo = new QComboBox();
|
||||||
|
targetCombo->setEnabled(false);
|
||||||
|
auto nodesResult = m_client->ListNodes();
|
||||||
|
if (nodesResult.ok()) {
|
||||||
|
for (const auto &node : nodesResult.value) {
|
||||||
|
if (node.media_class.find("Sink") != std::string::npos ||
|
||||||
|
node.media_class.find("Source") != std::string::npos) {
|
||||||
|
QString label = QString::fromStdString(
|
||||||
|
node.description.empty() ? node.name : node.description);
|
||||||
|
targetCombo->addItem(label, QString::fromStdString(node.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto *targetLabel = new QLabel(QStringLiteral("Target Node:"));
|
||||||
|
targetLabel->setEnabled(false);
|
||||||
|
form->addRow(targetLabel, targetCombo);
|
||||||
|
|
||||||
|
connect(loopbackCheck, &QCheckBox::toggled, this, [=](bool checked) {
|
||||||
|
targetCombo->setEnabled(checked);
|
||||||
|
targetLabel->setEnabled(checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto *buttons = new QDialogButtonBox(
|
||||||
|
QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
buttons->setStyleSheet(kButtonStyle);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
|
||||||
|
form->addRow(buttons);
|
||||||
|
|
||||||
|
if (dlg.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString name = nameEdit->text().trimmed();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QStringLiteral("Name cannot be empty."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string nodeName = name.toStdString();
|
||||||
m_model->setPendingPosition(nodeName, scenePos);
|
m_model->setPendingPosition(nodeName, scenePos);
|
||||||
|
|
||||||
warppipe::Status status;
|
warppipe::VirtualNodeOptions opts;
|
||||||
if (isSink) {
|
if (loopbackCheck->isChecked()) {
|
||||||
auto result = m_client->CreateVirtualSink(nodeName);
|
opts.behavior = warppipe::VirtualBehavior::kLoopback;
|
||||||
status = result.status;
|
opts.target_node = targetCombo->currentData().toString().toStdString();
|
||||||
} else {
|
|
||||||
auto result = m_client->CreateVirtualSource(nodeName);
|
|
||||||
status = result.status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!status.ok()) {
|
auto result = m_client->CreateVirtualSource(nodeName, opts);
|
||||||
|
if (!result.status.ok()) {
|
||||||
QMessageBox::warning(this, QStringLiteral("Error"),
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
QString::fromStdString(status.message));
|
QString::fromStdString(result.status.message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_model->refreshFromClient();
|
m_model->refreshFromClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1814,3 +1920,321 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
||||||
m_client->AddRouteRule(rule);
|
m_client->AddRouteRule(rule);
|
||||||
rebuildRulesList();
|
rebuildRulesList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::onSelectionChanged() {
|
||||||
|
auto items = m_scene->selectedItems();
|
||||||
|
|
||||||
|
QtNodes::NodeId selected = 0;
|
||||||
|
for (QGraphicsItem *item : items) {
|
||||||
|
if (auto *ngo =
|
||||||
|
qgraphicsitem_cast<QtNodes::NodeGraphicsObject *>(item)) {
|
||||||
|
selected = ngo->nodeId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected == m_selectedNodeId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (selected != 0) {
|
||||||
|
m_selectedNodeId = selected;
|
||||||
|
updateNodeDetailsPanel(selected);
|
||||||
|
m_sidebar->setCurrentWidget(m_nodeDetailsScroll);
|
||||||
|
} else {
|
||||||
|
m_selectedNodeId = 0;
|
||||||
|
clearNodeDetailsPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::clearNodeDetailsPanel() {
|
||||||
|
delete m_nodeDetailsScroll->takeWidget();
|
||||||
|
m_nodeDetailsContainer = new QWidget();
|
||||||
|
m_nodeDetailsContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;"));
|
||||||
|
auto *layout = new QVBoxLayout(m_nodeDetailsContainer);
|
||||||
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
layout->setSpacing(6);
|
||||||
|
auto *label = new QLabel(QStringLiteral("Select a node to view details"));
|
||||||
|
label->setStyleSheet(QStringLiteral(
|
||||||
|
"color: #6a6a7a; font-style: italic; background: transparent;"));
|
||||||
|
label->setAlignment(Qt::AlignCenter);
|
||||||
|
label->setWordWrap(true);
|
||||||
|
layout->addStretch();
|
||||||
|
layout->addWidget(label);
|
||||||
|
layout->addStretch();
|
||||||
|
m_nodeDetailsScroll->setWidget(m_nodeDetailsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QLabel *makeDetailHeader(const QString &text) {
|
||||||
|
auto *label = new QLabel(text);
|
||||||
|
label->setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;"
|
||||||
|
" background: transparent; }"));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QLabel *makeDetailValue(const QString &text) {
|
||||||
|
auto *label = new QLabel(text);
|
||||||
|
label->setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { color: #ecf0f6; font-size: 12px; background: transparent; }"));
|
||||||
|
label->setWordWrap(true);
|
||||||
|
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
|
||||||
|
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
||||||
|
if (!data)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto &info = data->info;
|
||||||
|
WarpNodeType type = WarpGraphModel::classifyNode(info);
|
||||||
|
bool isVirtual = type == WarpNodeType::kVirtualSink ||
|
||||||
|
type == WarpNodeType::kVirtualSource;
|
||||||
|
|
||||||
|
delete m_nodeDetailsScroll->takeWidget();
|
||||||
|
m_nodeDetailsContainer = new QWidget();
|
||||||
|
m_nodeDetailsContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;"));
|
||||||
|
auto *layout = new QVBoxLayout(m_nodeDetailsContainer);
|
||||||
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
layout->setSpacing(4);
|
||||||
|
|
||||||
|
auto addField = [&](const QString &header, const QString &value) {
|
||||||
|
if (value.isEmpty())
|
||||||
|
return;
|
||||||
|
layout->addSpacing(4);
|
||||||
|
layout->addWidget(makeDetailHeader(header));
|
||||||
|
layout->addWidget(makeDetailValue(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
QString typeLabel;
|
||||||
|
switch (type) {
|
||||||
|
case WarpNodeType::kHardwareSink: typeLabel = QStringLiteral("Hardware Sink"); break;
|
||||||
|
case WarpNodeType::kHardwareSource: typeLabel = QStringLiteral("Hardware Source"); break;
|
||||||
|
case WarpNodeType::kVirtualSink: typeLabel = QStringLiteral("Virtual Sink"); break;
|
||||||
|
case WarpNodeType::kVirtualSource: typeLabel = QStringLiteral("Virtual Source"); break;
|
||||||
|
case WarpNodeType::kApplication: typeLabel = QStringLiteral("Application"); break;
|
||||||
|
case WarpNodeType::kVideoSource: typeLabel = QStringLiteral("Video Source"); break;
|
||||||
|
case WarpNodeType::kVideoSink: typeLabel = QStringLiteral("Video Sink"); break;
|
||||||
|
default: typeLabel = QStringLiteral("Unknown"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *titleLabel = new QLabel(QString::fromStdString(
|
||||||
|
info.description.empty() ? info.name : info.description));
|
||||||
|
titleLabel->setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { color: #ecf0f6; font-size: 14px; font-weight: bold;"
|
||||||
|
" background: transparent; }"));
|
||||||
|
titleLabel->setWordWrap(true);
|
||||||
|
layout->addWidget(titleLabel);
|
||||||
|
layout->addSpacing(2);
|
||||||
|
|
||||||
|
auto *typeBadge = new QLabel(typeLabel);
|
||||||
|
typeBadge->setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { color: #1a1a1e; background: %1; font-size: 10px;"
|
||||||
|
" font-weight: bold; border-radius: 3px; padding: 2px 6px; }")
|
||||||
|
.arg(isVirtual ? QStringLiteral("#4caf50")
|
||||||
|
: QStringLiteral("#6a7a8a")));
|
||||||
|
typeBadge->setFixedHeight(18);
|
||||||
|
typeBadge->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
|
||||||
|
layout->addWidget(typeBadge);
|
||||||
|
|
||||||
|
static const QString kEditStyle = QStringLiteral(
|
||||||
|
"QLineEdit { background: #2a2a32; color: #ecf0f6;"
|
||||||
|
" border: 1px solid #3a3a44; border-radius: 4px; padding: 4px 8px; }");
|
||||||
|
static const QString kComboStyle = QStringLiteral(
|
||||||
|
"QComboBox { background: #2a2a32; color: #ecf0f6;"
|
||||||
|
" border: 1px solid #3a3a44; border-radius: 4px; padding: 4px 8px; }"
|
||||||
|
"QComboBox::drop-down { border: none; }"
|
||||||
|
"QComboBox QAbstractItemView { background: #2a2a32; color: #ecf0f6;"
|
||||||
|
" selection-background-color: #3a3a44; }");
|
||||||
|
static const QString kCheckStyle = QStringLiteral(
|
||||||
|
"QCheckBox { color: #ecf0f6; background: transparent; }"
|
||||||
|
"QCheckBox::indicator { width: 16px; height: 16px; }");
|
||||||
|
static const QString kBtnStyle = QStringLiteral(
|
||||||
|
"QPushButton { background: #2e2e36; color: #ecf0f6;"
|
||||||
|
" border: 1px solid #3a3a44; border-radius: 4px;"
|
||||||
|
" padding: 6px 16px; font-weight: bold; }"
|
||||||
|
"QPushButton:hover { background: #3a3a44; }");
|
||||||
|
static const QString kDeleteBtnStyle = QStringLiteral(
|
||||||
|
"QPushButton { background: #b03030; color: #ecf0f6;"
|
||||||
|
" border: 1px solid #d04040; border-radius: 4px;"
|
||||||
|
" padding: 6px 16px; font-weight: bold; }"
|
||||||
|
"QPushButton:hover { background: #c04040; }");
|
||||||
|
|
||||||
|
if (isVirtual) {
|
||||||
|
bool isSource = type == WarpNodeType::kVirtualSource;
|
||||||
|
auto vnResult = m_client->GetVirtualNodeInfo(info.id);
|
||||||
|
|
||||||
|
layout->addSpacing(4);
|
||||||
|
layout->addWidget(makeDetailHeader(QStringLiteral("NAME")));
|
||||||
|
auto *nameEdit = new QLineEdit(QString::fromStdString(info.name));
|
||||||
|
nameEdit->setStyleSheet(kEditStyle);
|
||||||
|
layout->addWidget(nameEdit);
|
||||||
|
|
||||||
|
QCheckBox *loopbackCheck = nullptr;
|
||||||
|
QComboBox *targetCombo = nullptr;
|
||||||
|
|
||||||
|
if (isSource) {
|
||||||
|
layout->addSpacing(8);
|
||||||
|
loopbackCheck = new QCheckBox(QStringLiteral("Loopback from another node"));
|
||||||
|
loopbackCheck->setStyleSheet(kCheckStyle);
|
||||||
|
layout->addWidget(loopbackCheck);
|
||||||
|
|
||||||
|
layout->addSpacing(4);
|
||||||
|
layout->addWidget(makeDetailHeader(QStringLiteral("TARGET NODE")));
|
||||||
|
targetCombo = new QComboBox();
|
||||||
|
targetCombo->setStyleSheet(kComboStyle);
|
||||||
|
targetCombo->setEnabled(false);
|
||||||
|
auto nodesResult = m_client->ListNodes();
|
||||||
|
if (nodesResult.ok()) {
|
||||||
|
for (const auto &node : nodesResult.value) {
|
||||||
|
if (node.id.value == info.id.value)
|
||||||
|
continue;
|
||||||
|
if (node.media_class.find("Sink") != std::string::npos ||
|
||||||
|
node.media_class.find("Source") != std::string::npos) {
|
||||||
|
QString label = QString::fromStdString(
|
||||||
|
node.description.empty() ? node.name : node.description);
|
||||||
|
targetCombo->addItem(label, QString::fromStdString(node.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout->addWidget(targetCombo);
|
||||||
|
|
||||||
|
if (vnResult.ok() && vnResult.value.loopback) {
|
||||||
|
loopbackCheck->setChecked(true);
|
||||||
|
int idx = targetCombo->findData(
|
||||||
|
QString::fromStdString(vnResult.value.target_node));
|
||||||
|
if (idx >= 0)
|
||||||
|
targetCombo->setCurrentIndex(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
|
||||||
|
&QWidget::setEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout->addSpacing(12);
|
||||||
|
auto *applyBtn = new QPushButton(QStringLiteral("Apply Changes"));
|
||||||
|
applyBtn->setStyleSheet(kBtnStyle);
|
||||||
|
connect(applyBtn, &QPushButton::clicked, this,
|
||||||
|
[this, nodeId, nameEdit, loopbackCheck, targetCombo, isSource]() {
|
||||||
|
const WarpNodeData *d = m_model->warpNodeData(nodeId);
|
||||||
|
if (!d) return;
|
||||||
|
|
||||||
|
QString newName = nameEdit->text().trimmed();
|
||||||
|
if (newName.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QStringLiteral("Name cannot be empty."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF pos = m_model->nodeData(nodeId,
|
||||||
|
QtNodes::NodeRole::Position).toPointF();
|
||||||
|
|
||||||
|
auto removeStatus = m_client->RemoveNode(d->info.id);
|
||||||
|
if (!removeStatus.ok()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QString::fromStdString(removeStatus.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string nodeName = newName.toStdString();
|
||||||
|
m_model->setPendingPosition(nodeName, pos);
|
||||||
|
|
||||||
|
warppipe::Status status;
|
||||||
|
if (isSource) {
|
||||||
|
warppipe::VirtualNodeOptions opts;
|
||||||
|
if (loopbackCheck && loopbackCheck->isChecked() && targetCombo) {
|
||||||
|
opts.behavior = warppipe::VirtualBehavior::kLoopback;
|
||||||
|
opts.target_node =
|
||||||
|
targetCombo->currentData().toString().toStdString();
|
||||||
|
}
|
||||||
|
auto result = m_client->CreateVirtualSource(nodeName, opts);
|
||||||
|
status = result.status;
|
||||||
|
} else {
|
||||||
|
auto result = m_client->CreateVirtualSink(nodeName);
|
||||||
|
status = result.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.ok()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QString::fromStdString(status.message));
|
||||||
|
m_model->refreshFromClient();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedNodeId = 0;
|
||||||
|
m_model->refreshFromClient();
|
||||||
|
|
||||||
|
for (auto nid : m_model->allNodeIds()) {
|
||||||
|
const WarpNodeData *nd = m_model->warpNodeData(nid);
|
||||||
|
if (nd && nd->info.name == nodeName) {
|
||||||
|
m_selectedNodeId = nid;
|
||||||
|
updateNodeDetailsPanel(nid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto *mw = qobject_cast<QMainWindow *>(window()))
|
||||||
|
mw->statusBar()->showMessage(
|
||||||
|
QStringLiteral("Virtual node updated"), 4000);
|
||||||
|
});
|
||||||
|
layout->addWidget(applyBtn);
|
||||||
|
|
||||||
|
layout->addSpacing(8);
|
||||||
|
auto *deleteBtn = new QPushButton(QStringLiteral("Delete Node"));
|
||||||
|
deleteBtn->setStyleSheet(kDeleteBtnStyle);
|
||||||
|
connect(deleteBtn, &QPushButton::clicked, this, [this, nodeId]() {
|
||||||
|
const WarpNodeData *d = m_model->warpNodeData(nodeId);
|
||||||
|
if (!d) return;
|
||||||
|
auto status = m_client->RemoveNode(d->info.id);
|
||||||
|
if (status.ok()) {
|
||||||
|
m_model->refreshFromClient();
|
||||||
|
clearNodeDetailsPanel();
|
||||||
|
m_selectedNodeId = 0;
|
||||||
|
} else {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||||
|
QString::fromStdString(status.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout->addWidget(deleteBtn);
|
||||||
|
} else {
|
||||||
|
addField(QStringLiteral("NAME"), QString::fromStdString(info.name));
|
||||||
|
if (!info.description.empty() && info.description != info.name)
|
||||||
|
addField(QStringLiteral("DESCRIPTION"),
|
||||||
|
QString::fromStdString(info.description));
|
||||||
|
addField(QStringLiteral("MEDIA CLASS"),
|
||||||
|
QString::fromStdString(info.media_class));
|
||||||
|
if (!info.application_name.empty())
|
||||||
|
addField(QStringLiteral("APPLICATION"),
|
||||||
|
QString::fromStdString(info.application_name));
|
||||||
|
if (!info.process_binary.empty())
|
||||||
|
addField(QStringLiteral("PROCESS"),
|
||||||
|
QString::fromStdString(info.process_binary));
|
||||||
|
if (!info.media_role.empty())
|
||||||
|
addField(QStringLiteral("MEDIA ROLE"),
|
||||||
|
QString::fromStdString(info.media_role));
|
||||||
|
}
|
||||||
|
|
||||||
|
addField(QStringLiteral("NODE ID"),
|
||||||
|
QString::number(info.id.value));
|
||||||
|
|
||||||
|
if (!data->inputPorts.empty()) {
|
||||||
|
layout->addSpacing(8);
|
||||||
|
layout->addWidget(makeDetailHeader(QStringLiteral("INPUT PORTS")));
|
||||||
|
for (const auto &port : data->inputPorts) {
|
||||||
|
layout->addWidget(makeDetailValue(
|
||||||
|
QStringLiteral(" ") + QString::fromStdString(port.name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data->outputPorts.empty()) {
|
||||||
|
layout->addSpacing(8);
|
||||||
|
layout->addWidget(makeDetailHeader(QStringLiteral("OUTPUT PORTS")));
|
||||||
|
for (const auto &port : data->outputPorts) {
|
||||||
|
layout->addWidget(makeDetailValue(
|
||||||
|
QStringLiteral(" ") + QString::fromStdString(port.name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout->addStretch();
|
||||||
|
m_nodeDetailsScroll->setWidget(m_nodeDetailsContainer);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@ private:
|
||||||
const std::string &prefillTarget = {},
|
const std::string &prefillTarget = {},
|
||||||
warppipe::RuleId editRuleId = {});
|
warppipe::RuleId editRuleId = {});
|
||||||
void setConnectionStyle(ConnectionStyleType style);
|
void setConnectionStyle(ConnectionStyleType style);
|
||||||
|
void onSelectionChanged();
|
||||||
|
void updateNodeDetailsPanel(QtNodes::NodeId nodeId);
|
||||||
|
void clearNodeDetailsPanel();
|
||||||
|
|
||||||
struct PendingPasteLink {
|
struct PendingPasteLink {
|
||||||
std::string outNodeName;
|
std::string outNodeName;
|
||||||
|
|
@ -138,4 +141,8 @@ private:
|
||||||
QLabel *m_zoomMinValue = nullptr;
|
QLabel *m_zoomMinValue = nullptr;
|
||||||
QSlider *m_zoomMaxSlider = nullptr;
|
QSlider *m_zoomMaxSlider = nullptr;
|
||||||
QLabel *m_zoomMaxValue = nullptr;
|
QLabel *m_zoomMaxValue = nullptr;
|
||||||
|
|
||||||
|
QWidget *m_nodeDetailsContainer = nullptr;
|
||||||
|
QScrollArea *m_nodeDetailsScroll = nullptr;
|
||||||
|
QtNodes::NodeId m_selectedNodeId = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,16 @@ struct RouteRule {
|
||||||
std::string target_node;
|
std::string target_node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct VirtualNodeInfo {
|
||||||
|
NodeId node;
|
||||||
|
std::string name;
|
||||||
|
bool is_source = false;
|
||||||
|
bool loopback = false;
|
||||||
|
std::string target_node;
|
||||||
|
uint32_t rate = 48000;
|
||||||
|
uint32_t channels = 2;
|
||||||
|
};
|
||||||
|
|
||||||
struct VolumeState {
|
struct VolumeState {
|
||||||
float volume = 1.0f;
|
float volume = 1.0f;
|
||||||
bool mute = false;
|
bool mute = false;
|
||||||
|
|
@ -176,6 +186,7 @@ class Client {
|
||||||
Result<VirtualSource> CreateVirtualSource(std::string_view name,
|
Result<VirtualSource> CreateVirtualSource(std::string_view name,
|
||||||
const VirtualNodeOptions& options = VirtualNodeOptions{});
|
const VirtualNodeOptions& options = VirtualNodeOptions{});
|
||||||
Status RemoveNode(NodeId node);
|
Status RemoveNode(NodeId node);
|
||||||
|
Result<VirtualNodeInfo> GetVirtualNodeInfo(NodeId node) const;
|
||||||
|
|
||||||
Status SetNodeVolume(NodeId node, float volume, bool mute);
|
Status SetNodeVolume(NodeId node, float volume, bool mute);
|
||||||
Result<VolumeState> GetNodeVolume(NodeId node) const;
|
Result<VolumeState> GetNodeVolume(NodeId node) const;
|
||||||
|
|
|
||||||
|
|
@ -2140,9 +2140,28 @@ Status Client::DisableNodeMeter(NodeId node) {
|
||||||
meter->stream = nullptr;
|
meter->stream = nullptr;
|
||||||
pw_thread_loop_unlock(impl_->thread_loop);
|
pw_thread_loop_unlock(impl_->thread_loop);
|
||||||
}
|
}
|
||||||
|
impl_->AutoSave();
|
||||||
return Status::Ok();
|
return Status::Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<VirtualNodeInfo> Client::GetVirtualNodeInfo(NodeId node) const {
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
|
auto it = impl_->virtual_streams.find(node.value);
|
||||||
|
if (it == impl_->virtual_streams.end())
|
||||||
|
return {Status::Error(StatusCode::kNotFound, "not a virtual node"), {}};
|
||||||
|
const auto &sd = *it->second;
|
||||||
|
VirtualNodeInfo info;
|
||||||
|
info.node = node;
|
||||||
|
info.name = sd.name;
|
||||||
|
info.is_source = sd.is_source;
|
||||||
|
info.loopback = sd.loopback;
|
||||||
|
info.target_node = sd.target_node;
|
||||||
|
info.rate = sd.rate;
|
||||||
|
info.channels = sd.channels;
|
||||||
|
return {Status::Ok(), std::move(info)};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Result<MeterState> Client::NodeMeterPeak(NodeId node) const {
|
Result<MeterState> Client::NodeMeterPeak(NodeId node) const {
|
||||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||||
auto live_it = impl_->live_meters.find(node.value);
|
auto live_it = impl_->live_meters.find(node.value);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue