Compare commits
No commits in common. "0dbd10b5e3c413141cec268c46f1d8a73ad3f02b" and "d389161f4aa3907bb01d24ab728013c168ec1581" have entirely different histories.
0dbd10b5e3
...
d389161f4a
7 changed files with 55 additions and 749 deletions
|
|
@ -15,7 +15,6 @@
|
|||
#include <QtNodes/internal/UndoCommands.hpp>
|
||||
|
||||
#include <QAction>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QComboBox>
|
||||
|
|
@ -182,13 +181,6 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
||||
m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
|
||||
|
||||
connect(m_model, &WarpGraphModel::beginBatchUpdate, this, [this]() {
|
||||
m_scene->setItemIndexMethod(QGraphicsScene::NoIndex);
|
||||
});
|
||||
connect(m_model, &WarpGraphModel::endBatchUpdate, this, [this]() {
|
||||
m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
|
||||
});
|
||||
|
||||
QtNodes::ConnectionStyle::setConnectionStyle(
|
||||
R"({"ConnectionStyle": {
|
||||
"ConstructionColor": "#b4b4c8",
|
||||
|
|
@ -428,25 +420,6 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
m_rulesScroll->setWidget(m_rulesContainer);
|
||||
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->addWidget(m_view);
|
||||
m_splitter->addWidget(m_sidebar);
|
||||
|
|
@ -576,15 +549,8 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
|||
rebuildMixerStrips();
|
||||
rebuildNodeMeters();
|
||||
rebuildRulesList();
|
||||
if (nodeId == m_selectedNodeId) {
|
||||
m_selectedNodeId = 0;
|
||||
clearNodeDetailsPanel();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_scene, &QGraphicsScene::selectionChanged, this,
|
||||
&GraphEditorWidget::onSelectionChanged);
|
||||
|
||||
m_saveTimer = new QTimer(this);
|
||||
m_saveTimer->setSingleShot(true);
|
||||
m_saveTimer->setInterval(1000);
|
||||
|
|
@ -651,10 +617,6 @@ GraphEditorWidget::~GraphEditorWidget() {
|
|||
if (m_client) {
|
||||
m_client->SetChangeCallback(nullptr);
|
||||
}
|
||||
m_meterTimer->stop();
|
||||
m_refreshTimer->stop();
|
||||
m_changeTimer->stop();
|
||||
m_saveTimer->stop();
|
||||
}
|
||||
|
||||
int GraphEditorWidget::nodeCount() const {
|
||||
|
|
@ -892,9 +854,6 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
|||
createRuleAction = menu.addAction(QStringLiteral("Create Rule..."));
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
QAction *detailsAction = menu.addAction(QStringLiteral("Node Details"));
|
||||
|
||||
menu.addSeparator();
|
||||
QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
|
||||
pasteAction->setShortcut(QKeySequence::Paste);
|
||||
|
|
@ -914,10 +873,6 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
|||
duplicateSelection();
|
||||
} else if (chosen == deleteAction && m_client) {
|
||||
deleteSelection();
|
||||
} else if (chosen == detailsAction) {
|
||||
m_selectedNodeId = qtNodeId;
|
||||
updateNodeDetailsPanel(qtNodeId);
|
||||
m_sidebar->setCurrentWidget(m_nodeDetailsScroll);
|
||||
} else if (chosen == pasteAction) {
|
||||
pasteSelection(QPointF(0, 0));
|
||||
} else if (chosen == createRuleAction) {
|
||||
|
|
@ -929,113 +884,34 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
|||
|
||||
void GraphEditorWidget::createVirtualNode(bool isSink,
|
||||
const QPointF &scenePos) {
|
||||
if (isSink) {
|
||||
bool ok = false;
|
||||
QString name = QInputDialog::getText(
|
||||
this, QStringLiteral("Create Virtual Sink"),
|
||||
QStringLiteral("Node name:"), QLineEdit::Normal, QString(), &ok);
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
std::string nodeName = name.trimmed().toStdString();
|
||||
m_model->setPendingPosition(nodeName, scenePos);
|
||||
|
||||
warppipe::VirtualNodeOptions opts;
|
||||
if (loopbackCheck->isChecked()) {
|
||||
opts.behavior = warppipe::VirtualBehavior::kLoopback;
|
||||
opts.target_node = targetCombo->currentData().toString().toStdString();
|
||||
warppipe::Status status;
|
||||
if (isSink) {
|
||||
auto result = m_client->CreateVirtualSink(nodeName);
|
||||
status = result.status;
|
||||
} else {
|
||||
auto result = m_client->CreateVirtualSource(nodeName);
|
||||
status = result.status;
|
||||
}
|
||||
|
||||
auto result = m_client->CreateVirtualSource(nodeName, opts);
|
||||
if (!result.status.ok()) {
|
||||
if (!status.ok()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Error"),
|
||||
QString::fromStdString(result.status.message));
|
||||
QString::fromStdString(status.message));
|
||||
return;
|
||||
}
|
||||
|
||||
m_model->refreshFromClient();
|
||||
}
|
||||
|
||||
|
|
@ -1938,319 +1814,3 @@ void GraphEditorWidget::showAddRuleDialog(const std::string &prefillApp,
|
|||
m_client->AddRouteRule(rule);
|
||||
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;
|
||||
|
||||
m_selectedNodeId = selected;
|
||||
if (selected != 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
|
||||
updateNodeDetailsPanel(selected);
|
||||
} else if (selected == 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
|
||||
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);
|
||||
|
||||
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
|
||||
&QWidget::setEnabled);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,9 +87,6 @@ private:
|
|||
const std::string &prefillTarget = {},
|
||||
warppipe::RuleId editRuleId = {});
|
||||
void setConnectionStyle(ConnectionStyleType style);
|
||||
void onSelectionChanged();
|
||||
void updateNodeDetailsPanel(QtNodes::NodeId nodeId);
|
||||
void clearNodeDetailsPanel();
|
||||
|
||||
struct PendingPasteLink {
|
||||
std::string outNodeName;
|
||||
|
|
@ -141,8 +138,4 @@ private:
|
|||
QLabel *m_zoomMinValue = nullptr;
|
||||
QSlider *m_zoomMaxSlider = nullptr;
|
||||
QLabel *m_zoomMaxValue = nullptr;
|
||||
|
||||
QWidget *m_nodeDetailsContainer = nullptr;
|
||||
QScrollArea *m_nodeDetailsScroll = nullptr;
|
||||
QtNodes::NodeId m_selectedNodeId = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -208,8 +208,8 @@ QVariant WarpGraphModel::nodeData(QtNodes::NodeId nodeId,
|
|||
}
|
||||
case QtNodes::NodeRole::Widget: {
|
||||
auto wIt = m_volumeWidgets.find(nodeId);
|
||||
if (wIt != m_volumeWidgets.end() && wIt->second)
|
||||
return QVariant::fromValue(wIt->second.data());
|
||||
if (wIt != m_volumeWidgets.end())
|
||||
return QVariant::fromValue(wIt->second);
|
||||
return QVariant::fromValue(static_cast<QWidget *>(nullptr));
|
||||
}
|
||||
default:
|
||||
|
|
@ -329,7 +329,11 @@ bool WarpGraphModel::deleteNode(QtNodes::NodeId const nodeId) {
|
|||
m_sizes.erase(nodeId);
|
||||
m_volumeStates.erase(nodeId);
|
||||
m_styleCache.erase(nodeId);
|
||||
m_volumeWidgets.erase(nodeId);
|
||||
auto vwIt = m_volumeWidgets.find(nodeId);
|
||||
if (vwIt != m_volumeWidgets.end()) {
|
||||
delete vwIt->second;
|
||||
m_volumeWidgets.erase(vwIt);
|
||||
}
|
||||
Q_EMIT nodeDeleted(nodeId);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -352,12 +356,9 @@ void WarpGraphModel::refreshFromClient() {
|
|||
return;
|
||||
}
|
||||
|
||||
Q_EMIT beginBatchUpdate();
|
||||
m_refreshing = true;
|
||||
auto nodesResult = m_client->ListNodes();
|
||||
if (!nodesResult.ok()) {
|
||||
m_refreshing = false;
|
||||
Q_EMIT endBatchUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -488,23 +489,18 @@ void WarpGraphModel::refreshFromClient() {
|
|||
m_pwToQt.emplace(nodeInfo.id.value, qtId);
|
||||
|
||||
auto pendingIt = m_pendingPositions.find(nodeInfo.name);
|
||||
if (pendingIt != m_pendingPositions.end()) {
|
||||
m_positions.emplace(qtId, pendingIt->second);
|
||||
m_pendingPositions.erase(pendingIt);
|
||||
if (pendingIt != m_pendingPositions.end()) {
|
||||
m_positions.emplace(qtId, pendingIt->second);
|
||||
m_pendingPositions.erase(pendingIt);
|
||||
} else {
|
||||
auto savedIt = m_savedPositions.find(nodeInfo.name);
|
||||
if (savedIt != m_savedPositions.end()) {
|
||||
m_positions.emplace(qtId, savedIt->second);
|
||||
} else {
|
||||
auto groupPos = findAppGroupPosition(nodeIt->second);
|
||||
if (groupPos) {
|
||||
m_positions.emplace(qtId, findNonOverlappingPosition(*groupPos, nodeIt->second));
|
||||
} else {
|
||||
auto savedIt = m_savedPositions.find(nodeInfo.name);
|
||||
if (savedIt != m_savedPositions.end()) {
|
||||
m_positions.emplace(qtId, savedIt->second);
|
||||
} else {
|
||||
QPointF candidate = nextPosition(nodeIt->second);
|
||||
m_positions.emplace(qtId, findNonOverlappingPosition(candidate, nodeIt->second));
|
||||
}
|
||||
}
|
||||
QPointF candidate = nextPosition(nodeIt->second);
|
||||
m_positions.emplace(qtId, findNonOverlappingPosition(candidate, nodeIt->second));
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeHasVolume(nodeType)) {
|
||||
auto *volumeWidget = new NodeVolumeWidget();
|
||||
|
|
@ -684,8 +680,8 @@ void WarpGraphModel::refreshFromClient() {
|
|||
cached.mute = mute;
|
||||
|
||||
auto wIt = m_volumeWidgets.find(qtId);
|
||||
if (wIt != m_volumeWidgets.end() && wIt->second) {
|
||||
auto *vw = static_cast<NodeVolumeWidget *>(wIt->second.data());
|
||||
if (wIt != m_volumeWidgets.end()) {
|
||||
auto *vw = static_cast<NodeVolumeWidget *>(wIt->second);
|
||||
if (!vw->isSliderDown()) {
|
||||
vw->setVolume(sliderVal);
|
||||
vw->setMuted(mute);
|
||||
|
|
@ -696,7 +692,6 @@ void WarpGraphModel::refreshFromClient() {
|
|||
}
|
||||
|
||||
m_refreshing = false;
|
||||
Q_EMIT endBatchUpdate();
|
||||
}
|
||||
|
||||
const WarpNodeData *
|
||||
|
|
@ -768,61 +763,8 @@ QPointF WarpGraphModel::nextPosition(const WarpNodeData &data) {
|
|||
return pos;
|
||||
}
|
||||
|
||||
std::string WarpGraphModel::appGroupKey(const warppipe::NodeInfo &info) {
|
||||
if (!info.application_name.empty())
|
||||
return info.application_name;
|
||||
if (!info.process_binary.empty())
|
||||
return info.process_binary;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<QPointF> WarpGraphModel::findAppGroupPosition(const WarpNodeData &data) const {
|
||||
WarpNodeType type = classifyNode(data.info);
|
||||
if (type != WarpNodeType::kApplication)
|
||||
return std::nullopt;
|
||||
|
||||
std::string key = appGroupKey(data.info);
|
||||
if (key.empty())
|
||||
return std::nullopt;
|
||||
|
||||
double lowestBottom = -1.0;
|
||||
QPointF siblingPos;
|
||||
bool found = false;
|
||||
|
||||
for (const auto &[existingId, existingData] : m_nodes) {
|
||||
if (classifyNode(existingData.info) != WarpNodeType::kApplication)
|
||||
continue;
|
||||
if (appGroupKey(existingData.info) != key)
|
||||
continue;
|
||||
|
||||
auto posIt = m_positions.find(existingId);
|
||||
if (posIt == m_positions.end())
|
||||
continue;
|
||||
|
||||
QSizeF existingSize;
|
||||
auto sizeIt = m_sizes.find(existingId);
|
||||
if (sizeIt != m_sizes.end()) {
|
||||
existingSize = QSizeF(sizeIt->second);
|
||||
} else {
|
||||
existingSize = QSizeF(estimateNodeSize(existingData));
|
||||
}
|
||||
|
||||
double bottom = posIt->second.y() + existingSize.height();
|
||||
if (bottom > lowestBottom) {
|
||||
lowestBottom = bottom;
|
||||
siblingPos = posIt->second;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return std::nullopt;
|
||||
|
||||
return QPointF(siblingPos.x(), lowestBottom + kVerticalGap);
|
||||
}
|
||||
|
||||
QPointF WarpGraphModel::findNonOverlappingPosition(QPointF candidate,
|
||||
const WarpNodeData &data) const {
|
||||
const WarpNodeData &data) const {
|
||||
QSizeF newSize(estimateNodeSize(data));
|
||||
constexpr int kMaxAttempts = 50;
|
||||
|
||||
|
|
@ -859,22 +801,6 @@ bool WarpGraphModel::isGhost(QtNodes::NodeId nodeId) const {
|
|||
return m_ghostNodes.find(nodeId) != m_ghostNodes.end();
|
||||
}
|
||||
|
||||
bool WarpGraphModel::ghostConnectionExists(
|
||||
QtNodes::ConnectionId connectionId) const {
|
||||
return m_ghostConnections.find(connectionId) != m_ghostConnections.end();
|
||||
}
|
||||
|
||||
std::unordered_set<QtNodes::ConnectionId>
|
||||
WarpGraphModel::allGhostConnectionIds(QtNodes::NodeId nodeId) const {
|
||||
std::unordered_set<QtNodes::ConnectionId> result;
|
||||
for (const auto &conn : m_ghostConnections) {
|
||||
if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) {
|
||||
result.insert(conn);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t WarpGraphModel::findPwNodeIdByName(const std::string &name) const {
|
||||
for (const auto &[qtId, data] : m_nodes) {
|
||||
if (data.info.name == name) {
|
||||
|
|
@ -929,8 +855,8 @@ void WarpGraphModel::setNodeVolumeState(QtNodes::NodeId nodeId,
|
|||
}
|
||||
|
||||
auto wIt = m_volumeWidgets.find(nodeId);
|
||||
if (wIt != m_volumeWidgets.end() && wIt->second) {
|
||||
auto *w = qobject_cast<NodeVolumeWidget *>(wIt->second.data());
|
||||
if (wIt != m_volumeWidgets.end()) {
|
||||
auto *w = qobject_cast<NodeVolumeWidget *>(wIt->second);
|
||||
if (w) {
|
||||
w->setVolume(volumeToSlider(state.volume));
|
||||
w->setMuted(state.mute);
|
||||
|
|
@ -1249,19 +1175,6 @@ void WarpGraphModel::autoArrange() {
|
|||
}
|
||||
}
|
||||
|
||||
std::sort(apps.ids.begin(), apps.ids.end(),
|
||||
[this](QtNodes::NodeId a, QtNodes::NodeId b) {
|
||||
auto itA = m_nodes.find(a);
|
||||
auto itB = m_nodes.find(b);
|
||||
if (itA == m_nodes.end() || itB == m_nodes.end())
|
||||
return a < b;
|
||||
std::string keyA = appGroupKey(itA->second.info);
|
||||
std::string keyB = appGroupKey(itB->second.info);
|
||||
if (keyA != keyB)
|
||||
return keyA < keyB;
|
||||
return a < b;
|
||||
});
|
||||
|
||||
auto layoutColumn = [&](Column &col, double xOffset) {
|
||||
double y = 0.0;
|
||||
for (QtNodes::NodeId id : col.ids) {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@
|
|||
#include <QHash>
|
||||
#include <QPointF>
|
||||
#include <QSize>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
|
|
@ -79,9 +77,6 @@ public:
|
|||
const WarpNodeData *warpNodeData(QtNodes::NodeId nodeId) const;
|
||||
QtNodes::NodeId qtNodeIdForPw(uint32_t pwNodeId) const;
|
||||
bool isGhost(QtNodes::NodeId nodeId) const;
|
||||
bool ghostConnectionExists(QtNodes::ConnectionId connectionId) const;
|
||||
std::unordered_set<QtNodes::ConnectionId> allGhostConnectionIds(
|
||||
QtNodes::NodeId nodeId) const;
|
||||
void setPendingPosition(const std::string &nodeName, QPointF pos);
|
||||
static WarpNodeType classifyNode(const warppipe::NodeInfo &info);
|
||||
|
||||
|
|
@ -96,8 +91,6 @@ public:
|
|||
NodeVolumeState nodeVolumeState(QtNodes::NodeId nodeId) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void beginBatchUpdate();
|
||||
void endBatchUpdate();
|
||||
void nodeVolumeChanged(QtNodes::NodeId nodeId, NodeVolumeState previous,
|
||||
NodeVolumeState current);
|
||||
|
||||
|
|
@ -127,8 +120,6 @@ private:
|
|||
static QVariant styleForNode(WarpNodeType type, bool ghost);
|
||||
QPointF nextPosition(const WarpNodeData &data);
|
||||
QPointF findNonOverlappingPosition(QPointF candidate, const WarpNodeData &data) const;
|
||||
std::optional<QPointF> findAppGroupPosition(const WarpNodeData &data) const;
|
||||
static std::string appGroupKey(const warppipe::NodeInfo &info);
|
||||
static QSize estimateNodeSize(const WarpNodeData &data);
|
||||
|
||||
warppipe::Client *m_client = nullptr;
|
||||
|
|
@ -167,6 +158,6 @@ private:
|
|||
ViewState m_savedViewState{};
|
||||
|
||||
std::unordered_map<QtNodes::NodeId, NodeVolumeState> m_volumeStates;
|
||||
std::unordered_map<QtNodes::NodeId, QPointer<QWidget>> m_volumeWidgets;
|
||||
std::unordered_map<QtNodes::NodeId, QWidget *> m_volumeWidgets;
|
||||
mutable std::unordered_map<QtNodes::NodeId, QVariant> m_styleCache;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -138,16 +138,6 @@ struct RouteRule {
|
|||
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 {
|
||||
float volume = 1.0f;
|
||||
bool mute = false;
|
||||
|
|
@ -186,7 +176,6 @@ class Client {
|
|||
Result<VirtualSource> CreateVirtualSource(std::string_view name,
|
||||
const VirtualNodeOptions& options = VirtualNodeOptions{});
|
||||
Status RemoveNode(NodeId node);
|
||||
Result<VirtualNodeInfo> GetVirtualNodeInfo(NodeId node) const;
|
||||
|
||||
Status SetNodeVolume(NodeId node, float volume, bool mute);
|
||||
Result<VolumeState> GetNodeVolume(NodeId node) const;
|
||||
|
|
|
|||
164
src/warppipe.cpp
164
src/warppipe.cpp
|
|
@ -10,7 +10,6 @@
|
|||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
#include <pipewire/impl-module.h>
|
||||
#include <pipewire/keys.h>
|
||||
#include <pipewire/link.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
|
|
@ -105,7 +104,6 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
|
|||
|
||||
struct StreamData {
|
||||
pw_stream* stream = nullptr;
|
||||
pw_impl_module* module = nullptr;
|
||||
spa_hook listener{};
|
||||
pw_thread_loop* loop = nullptr;
|
||||
bool is_source = false;
|
||||
|
|
@ -351,8 +349,6 @@ struct Client::Impl {
|
|||
std::unordered_map<uint32_t, std::unique_ptr<NodeProxyData>> node_proxies;
|
||||
std::unordered_map<uint32_t, uint32_t> node_channel_counts;
|
||||
|
||||
std::unordered_set<uint32_t> loopback_internal_nodes;
|
||||
|
||||
std::unordered_map<uint32_t, MeterState> meter_states;
|
||||
std::unordered_set<uint32_t> metered_nodes;
|
||||
MeterState master_meter;
|
||||
|
|
@ -793,7 +789,6 @@ void Client::Impl::ClearCache() {
|
|||
nodes.clear();
|
||||
ports.clear();
|
||||
links.clear();
|
||||
loopback_internal_nodes.clear();
|
||||
pending_auto_links.clear();
|
||||
auto_link_claimed_pairs.clear();
|
||||
policy_sync_pending = false;
|
||||
|
|
@ -878,103 +873,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
|||
}
|
||||
}
|
||||
|
||||
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
|
||||
std::string args;
|
||||
args += "{ node.description = \"" + display_name + "\"";
|
||||
args += " node.name = \"" + stream_name + "\"";
|
||||
args += " audio.channels = " + std::to_string(options.format.channels);
|
||||
args += " audio.rate = " + std::to_string(options.format.rate);
|
||||
args += " capture.props = {";
|
||||
args += " target.object = \"" + *options.target_node + "\"";
|
||||
args += " stream.capture.sink = true";
|
||||
args += " node.passive = true";
|
||||
args += " node.dont-reconnect = true";
|
||||
args += " }";
|
||||
args += " playback.props = {";
|
||||
args += " node.name = \"" + stream_name + "\"";
|
||||
args += " node.description = \"" + display_name + "\"";
|
||||
args += " media.class = \"" + media_class_value + "\"";
|
||||
args += " node.virtual = true";
|
||||
args += " }";
|
||||
args += " }";
|
||||
|
||||
pw_impl_module* mod = pw_context_load_module(context,
|
||||
"libpipewire-module-loopback", args.c_str(), nullptr);
|
||||
if (!mod) {
|
||||
return {Status::Error(StatusCode::kUnavailable, "failed to load loopback module"), 0};
|
||||
}
|
||||
|
||||
auto sync_status = SyncLocked();
|
||||
if (!sync_status.ok()) {
|
||||
pw_impl_module_destroy(mod);
|
||||
return {sync_status, 0};
|
||||
}
|
||||
|
||||
uint32_t node_id = SPA_ID_INVALID;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||
for (const auto& entry : nodes) {
|
||||
if (entry.second.name == stream_name) {
|
||||
node_id = entry.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node_id == SPA_ID_INVALID) {
|
||||
int wait_attempts = 0;
|
||||
while (node_id == SPA_ID_INVALID && wait_attempts < 3) {
|
||||
int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds);
|
||||
if (wait_res == -ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
auto sync2 = SyncLocked();
|
||||
(void)sync2;
|
||||
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||
for (const auto& entry : nodes) {
|
||||
if (entry.second.name == stream_name) {
|
||||
node_id = entry.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
++wait_attempts;
|
||||
}
|
||||
}
|
||||
|
||||
if (node_id == SPA_ID_INVALID) {
|
||||
pw_impl_module_destroy(mod);
|
||||
return {Status::Error(StatusCode::kTimeout, "loopback node did not appear in registry"), 0};
|
||||
}
|
||||
|
||||
std::string capture_name = "input." + stream_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||
for (const auto& entry : nodes) {
|
||||
if (entry.second.name == capture_name) {
|
||||
loopback_internal_nodes.insert(entry.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto stream_data = std::make_unique<StreamData>();
|
||||
stream_data->module = mod;
|
||||
stream_data->loop = thread_loop;
|
||||
stream_data->is_source = is_source;
|
||||
stream_data->loopback = true;
|
||||
stream_data->target_node = *options.target_node;
|
||||
stream_data->name = stream_name;
|
||||
stream_data->rate = options.format.rate;
|
||||
stream_data->channels = options.format.channels;
|
||||
stream_data->node_id = node_id;
|
||||
stream_data->ready = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cache_mutex);
|
||||
virtual_streams.emplace(node_id, std::move(stream_data));
|
||||
}
|
||||
return {Status::Ok(), node_id};
|
||||
}
|
||||
|
||||
pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
|
||||
PW_KEY_MEDIA_CATEGORY, media_category,
|
||||
PW_KEY_MEDIA_ROLE, "Music",
|
||||
|
|
@ -990,6 +888,9 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
|||
if (node_group) {
|
||||
pw_properties_set(props, PW_KEY_NODE_GROUP, node_group);
|
||||
}
|
||||
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
|
||||
pw_properties_set(props, PW_KEY_TARGET_OBJECT, options.target_node->c_str());
|
||||
}
|
||||
|
||||
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
|
||||
if (!stream) {
|
||||
|
|
@ -1000,7 +901,7 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
|||
stream_data->stream = stream;
|
||||
stream_data->loop = thread_loop;
|
||||
stream_data->is_source = is_source;
|
||||
stream_data->loopback = false;
|
||||
stream_data->loopback = options.behavior == VirtualBehavior::kLoopback;
|
||||
if (options.target_node) {
|
||||
stream_data->target_node = *options.target_node;
|
||||
}
|
||||
|
|
@ -1025,6 +926,9 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
|
|||
|
||||
enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
|
||||
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
|
||||
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
|
||||
flags = static_cast<pw_stream_flags>(flags | PW_STREAM_FLAG_AUTOCONNECT);
|
||||
}
|
||||
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
|
||||
if (res < 0) {
|
||||
pw_stream_destroy(stream);
|
||||
|
|
@ -1138,11 +1042,7 @@ void Client::Impl::DisconnectLocked() {
|
|||
}
|
||||
for (auto& entry : streams) {
|
||||
StreamData* stream_data = entry.second.get();
|
||||
if (!stream_data) continue;
|
||||
if (stream_data->module) {
|
||||
pw_impl_module_destroy(stream_data->module);
|
||||
stream_data->module = nullptr;
|
||||
} else if (stream_data->stream) {
|
||||
if (stream_data && stream_data->stream) {
|
||||
pw_stream_disconnect(stream_data->stream);
|
||||
pw_stream_destroy(stream_data->stream);
|
||||
stream_data->stream = nullptr;
|
||||
|
|
@ -1927,9 +1827,6 @@ Result<std::vector<NodeInfo>> Client::ListNodes() {
|
|||
std::vector<NodeInfo> items;
|
||||
items.reserve(impl_->nodes.size());
|
||||
for (const auto& entry : impl_->nodes) {
|
||||
if (impl_->loopback_internal_nodes.count(entry.first)) {
|
||||
continue;
|
||||
}
|
||||
NodeInfo info = entry.second;
|
||||
if (!info.is_virtual &&
|
||||
impl_->virtual_streams.find(entry.first) !=
|
||||
|
|
@ -2036,28 +1933,10 @@ Status Client::RemoveNode(NodeId node) {
|
|||
owned_stream = std::move(it->second);
|
||||
impl_->virtual_streams.erase(it);
|
||||
}
|
||||
if (owned_stream) {
|
||||
if (owned_stream->module) {
|
||||
std::string capture_name = "input." + owned_stream->name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
for (auto it = impl_->loopback_internal_nodes.begin();
|
||||
it != impl_->loopback_internal_nodes.end(); ++it) {
|
||||
auto node_it = impl_->nodes.find(*it);
|
||||
if (node_it != impl_->nodes.end() &&
|
||||
node_it->second.name == capture_name) {
|
||||
impl_->loopback_internal_nodes.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pw_impl_module_destroy(owned_stream->module);
|
||||
owned_stream->module = nullptr;
|
||||
} else if (owned_stream->stream) {
|
||||
pw_stream_disconnect(owned_stream->stream);
|
||||
pw_stream_destroy(owned_stream->stream);
|
||||
owned_stream->stream = nullptr;
|
||||
}
|
||||
if (owned_stream && owned_stream->stream) {
|
||||
pw_stream_disconnect(owned_stream->stream);
|
||||
pw_stream_destroy(owned_stream->stream);
|
||||
owned_stream->stream = nullptr;
|
||||
}
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
impl_->AutoSave();
|
||||
|
|
@ -2261,28 +2140,9 @@ Status Client::DisableNodeMeter(NodeId node) {
|
|||
meter->stream = nullptr;
|
||||
pw_thread_loop_unlock(impl_->thread_loop);
|
||||
}
|
||||
impl_->AutoSave();
|
||||
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 {
|
||||
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
|
||||
auto live_it = impl_->live_meters.find(node.value);
|
||||
|
|
|
|||
|
|
@ -728,7 +728,7 @@ TEST_CASE("ghost connections preserved when node becomes ghost") {
|
|||
model.refreshFromClient();
|
||||
|
||||
REQUIRE(model.isGhost(appQt));
|
||||
REQUIRE(model.ghostConnectionExists(
|
||||
REQUIRE(model.connectionExists(
|
||||
QtNodes::ConnectionId{appQt, 0, sinkQt, 0}));
|
||||
}
|
||||
|
||||
|
|
@ -782,7 +782,7 @@ TEST_CASE("ghost connections survive save/load round-trip") {
|
|||
REQUIRE(appQt2 != 0);
|
||||
REQUIRE(model2.isGhost(appQt2));
|
||||
|
||||
auto conns = model2.allGhostConnectionIds(appQt2);
|
||||
auto conns = model2.allConnectionIds(appQt2);
|
||||
REQUIRE(conns.size() == 1);
|
||||
auto conn = *conns.begin();
|
||||
REQUIRE(conn.outNodeId == appQt2);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue