diff --git a/gui/GraphEditorWidget.cpp b/gui/GraphEditorWidget.cpp index 38849b7..1ebdb84 100644 --- a/gui/GraphEditorWidget.cpp +++ b/gui/GraphEditorWidget.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -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(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(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); -} diff --git a/gui/GraphEditorWidget.h b/gui/GraphEditorWidget.h index 52dc491..65921ad 100644 --- a/gui/GraphEditorWidget.h +++ b/gui/GraphEditorWidget.h @@ -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; }; diff --git a/gui/WarpGraphModel.cpp b/gui/WarpGraphModel.cpp index 2e4d022..263cec2 100644 --- a/gui/WarpGraphModel.cpp +++ b/gui/WarpGraphModel.cpp @@ -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(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(wIt->second.data()); + if (wIt != m_volumeWidgets.end()) { + auto *vw = static_cast(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 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 -WarpGraphModel::allGhostConnectionIds(QtNodes::NodeId nodeId) const { - std::unordered_set 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(wIt->second.data()); + if (wIt != m_volumeWidgets.end()) { + auto *w = qobject_cast(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) { diff --git a/gui/WarpGraphModel.h b/gui/WarpGraphModel.h index 7c09ca4..9a892a0 100644 --- a/gui/WarpGraphModel.h +++ b/gui/WarpGraphModel.h @@ -8,10 +8,8 @@ #include #include #include -#include #include -#include #include #include @@ -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 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 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 m_volumeStates; - std::unordered_map> m_volumeWidgets; + std::unordered_map m_volumeWidgets; mutable std::unordered_map m_styleCache; }; diff --git a/include/warppipe/warppipe.hpp b/include/warppipe/warppipe.hpp index a01a0e6..e04046e 100644 --- a/include/warppipe/warppipe.hpp +++ b/include/warppipe/warppipe.hpp @@ -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 CreateVirtualSource(std::string_view name, const VirtualNodeOptions& options = VirtualNodeOptions{}); Status RemoveNode(NodeId node); - Result GetVirtualNodeInfo(NodeId node) const; Status SetNodeVolume(NodeId node, float volume, bool mute); Result GetNodeVolume(NodeId node) const; diff --git a/src/warppipe.cpp b/src/warppipe.cpp index ba06c64..ec57de8 100644 --- a/src/warppipe.cpp +++ b/src/warppipe.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -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> node_proxies; std::unordered_map node_channel_counts; - std::unordered_set loopback_internal_nodes; - std::unordered_map meter_states; std::unordered_set 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 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 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 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 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(); - 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 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 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 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 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(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> Client::ListNodes() { std::vector 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 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 Client::GetVirtualNodeInfo(NodeId node) const { - std::lock_guard 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 Client::NodeMeterPeak(NodeId node) const { std::lock_guard lock(impl_->cache_mutex); auto live_it = impl_->live_meters.find(node.value); diff --git a/tests/gui/warppipe_gui_tests.cpp b/tests/gui/warppipe_gui_tests.cpp index 9e43a2c..8fd9971 100644 --- a/tests/gui/warppipe_gui_tests.cpp +++ b/tests/gui/warppipe_gui_tests.cpp @@ -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);