Compare commits
6 commits
f2d0494af2
...
f4132ee37c
| Author | SHA1 | Date | |
|---|---|---|---|
| f4132ee37c | |||
| b7cb84bb9b | |||
| b2ef476445 | |||
| 4796f6f5d7 | |||
| 453003cb25 | |||
| 05d6c06603 |
6 changed files with 656 additions and 10 deletions
|
|
@ -1240,9 +1240,9 @@ private:
|
||||||
**Estimated Time:** 1-2 weeks
|
**Estimated Time:** 1-2 weeks
|
||||||
- [x] Integrate QUndoStack for all graph operations
|
- [x] Integrate QUndoStack for all graph operations
|
||||||
- [x] Implement command classes for link, volume, node operations
|
- [x] Implement command classes for link, volume, node operations
|
||||||
- [ ] Add keyboard shortcuts (Delete, Ctrl+D, Ctrl+Z, etc.)
|
- [x] Add keyboard shortcuts (Delete, Ctrl+D, Ctrl+Z, etc.)
|
||||||
- [ ] Implement context menus for nodes/canvas
|
- [x] Implement context menus for nodes/canvas
|
||||||
- [ ] Add copy/paste/duplicate functionality
|
- [x] Add copy/paste/duplicate functionality
|
||||||
- [ ] **Acceptance Criteria:** Full undo/redo history, keyboard shortcuts work
|
- [ ] **Acceptance Criteria:** Full undo/redo history, keyboard shortcuts work
|
||||||
|
|
||||||
### Milestone 7: Error Handling & Edge Cases
|
### Milestone 7: Error Handling & Edge Cases
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,24 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUndoCommand>
|
#include <QUndoCommand>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
#include <QGraphicsItem>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QMimeData>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include <QtNodes/GraphicsViewStyle>
|
#include <QtNodes/GraphicsViewStyle>
|
||||||
#include <QtNodes/NodeStyle>
|
#include <QtNodes/NodeStyle>
|
||||||
|
#include <QtNodes/internal/NodeGraphicsObject.hpp>
|
||||||
|
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
|
||||||
|
#include <QtNodes/internal/UndoCommands.hpp>
|
||||||
|
|
||||||
#include "gui/ClickSlider.h"
|
#include "gui/ClickSlider.h"
|
||||||
|
|
||||||
|
|
@ -65,6 +74,144 @@ private:
|
||||||
NodeVolumeState m_next{};
|
NodeVolumeState m_next{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class DeleteVirtualNodeCommand : public QUndoCommand
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct VirtualNodeData {
|
||||||
|
uint32_t pipewireNodeId;
|
||||||
|
QtNodes::NodeId qtNodeId;
|
||||||
|
QString name;
|
||||||
|
QString description;
|
||||||
|
Potato::MediaClass mediaClass;
|
||||||
|
int channels;
|
||||||
|
int rate;
|
||||||
|
QPointF position;
|
||||||
|
NodeVolumeState volumeState;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeleteVirtualNodeCommand(GraphEditorWidget *widget,
|
||||||
|
QtNodes::BasicGraphicsScene *scene,
|
||||||
|
const QList<QtNodes::NodeId> &nodeIds)
|
||||||
|
: m_widget(widget)
|
||||||
|
, m_scene(scene)
|
||||||
|
{
|
||||||
|
if (!widget || !scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PipeWireGraphModel *model = widget->m_model;
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto nodeId : nodeIds) {
|
||||||
|
const Potato::NodeInfo *info = model->nodeInfo(nodeId);
|
||||||
|
if (!info || info->type != Potato::NodeType::Virtual) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualNodeData data;
|
||||||
|
data.pipewireNodeId = info->id;
|
||||||
|
data.qtNodeId = nodeId;
|
||||||
|
data.name = info->name;
|
||||||
|
data.description = info->description;
|
||||||
|
data.mediaClass = info->mediaClass;
|
||||||
|
const int inPortSize = static_cast<int>(info->inputPorts.size());
|
||||||
|
const int outPortSize = static_cast<int>(info->outputPorts.size());
|
||||||
|
data.channels = (inPortSize > outPortSize) ? inPortSize : outPortSize;
|
||||||
|
if (data.channels == 0) {
|
||||||
|
data.channels = 2;
|
||||||
|
}
|
||||||
|
data.rate = 48000;
|
||||||
|
data.position = model->nodeData(nodeId, QtNodes::NodeRole::Position).toPointF();
|
||||||
|
|
||||||
|
NodeVolumeState volumeState;
|
||||||
|
if (model->nodeVolumeState(info->id, volumeState)) {
|
||||||
|
data.volumeState = volumeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_virtualNodes.append(data);
|
||||||
|
m_nodeIds.append(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(QString("Delete Virtual Node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void undo() override
|
||||||
|
{
|
||||||
|
if (!m_widget || m_virtualNodes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *controller = m_widget->m_controller;
|
||||||
|
auto *model = m_widget->m_model;
|
||||||
|
if (!controller || !model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &data : m_virtualNodes) {
|
||||||
|
bool success = false;
|
||||||
|
if (data.mediaClass == Potato::MediaClass::AudioSource) {
|
||||||
|
success = controller->createVirtualSource(data.name, data.description, data.channels, data.rate);
|
||||||
|
} else {
|
||||||
|
success = controller->createVirtualSink(data.name, data.description, data.channels, data.rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
m_widget->m_pendingPastePositions.insert(data.name, data.position);
|
||||||
|
m_widget->m_pendingPasteVolumes.insert(data.name, data.volumeState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void redo() override
|
||||||
|
{
|
||||||
|
if (!m_widget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *controller = m_widget->m_controller;
|
||||||
|
PipeWireGraphModel *model = m_widget->m_model;
|
||||||
|
if (!controller || !model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_virtualNodes.size(); ++i) {
|
||||||
|
VirtualNodeData &data = m_virtualNodes[i];
|
||||||
|
|
||||||
|
const Potato::NodeInfo *nodeInfo = model->nodeInfo(data.qtNodeId);
|
||||||
|
if (nodeInfo && nodeInfo->type == Potato::NodeType::Virtual) {
|
||||||
|
controller->destroyVirtualNode(data.pipewireNodeId);
|
||||||
|
} else {
|
||||||
|
const QVector<Potato::NodeInfo> nodes = controller->nodes();
|
||||||
|
for (const auto &node : nodes) {
|
||||||
|
if (node.name == data.name && node.type == Potato::NodeType::Virtual) {
|
||||||
|
data.pipewireNodeId = node.id;
|
||||||
|
|
||||||
|
const auto allNodes = model->allNodeIds();
|
||||||
|
for (const auto qtId : allNodes) {
|
||||||
|
const Potato::NodeInfo *info = model->nodeInfo(qtId);
|
||||||
|
if (info && info->id == node.id) {
|
||||||
|
data.qtNodeId = qtId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller->destroyVirtualNode(node.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
GraphEditorWidget *m_widget = nullptr;
|
||||||
|
QtNodes::BasicGraphicsScene *m_scene = nullptr;
|
||||||
|
QList<VirtualNodeData> m_virtualNodes;
|
||||||
|
QList<QtNodes::NodeId> m_nodeIds;
|
||||||
|
};
|
||||||
|
|
||||||
GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent)
|
GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_controller(controller)
|
, m_controller(controller)
|
||||||
|
|
@ -106,6 +253,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
||||||
m_model->loadLayout();
|
m_model->loadLayout();
|
||||||
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
||||||
m_view = new QtNodes::GraphicsView(m_scene);
|
m_view = new QtNodes::GraphicsView(m_scene);
|
||||||
|
m_view->setFocusPolicy(Qt::StrongFocus);
|
||||||
|
m_view->viewport()->setFocusPolicy(Qt::StrongFocus);
|
||||||
m_scene->setBackgroundBrush(QColor(28, 30, 34));
|
m_scene->setBackgroundBrush(QColor(28, 30, 34));
|
||||||
|
|
||||||
m_splitter = new QSplitter(this);
|
m_splitter = new QSplitter(this);
|
||||||
|
|
@ -218,10 +367,38 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
||||||
|
|
||||||
m_view->setContextMenuPolicy(Qt::ActionsContextMenu);
|
m_view->setContextMenuPolicy(Qt::ActionsContextMenu);
|
||||||
|
|
||||||
|
removeDefaultCopyPasteActions();
|
||||||
|
|
||||||
auto *refreshAction = new QAction(QString("Refresh Graph"), m_view);
|
auto *refreshAction = new QAction(QString("Refresh Graph"), m_view);
|
||||||
connect(refreshAction, &QAction::triggered, this, &GraphEditorWidget::refreshGraph);
|
connect(refreshAction, &QAction::triggered, this, &GraphEditorWidget::refreshGraph);
|
||||||
m_view->addAction(refreshAction);
|
m_view->addAction(refreshAction);
|
||||||
|
|
||||||
|
auto *deleteAction = new QAction(QString("Delete Selection"), m_view);
|
||||||
|
deleteAction->setShortcut(QKeySequence::Delete);
|
||||||
|
deleteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(deleteAction, &QAction::triggered, this, &GraphEditorWidget::deleteSelection);
|
||||||
|
m_view->addAction(deleteAction);
|
||||||
|
|
||||||
|
auto *copyAction = new QAction(QString("Copy Selection"), m_view);
|
||||||
|
copyAction->setShortcut(QKeySequence::Copy);
|
||||||
|
copyAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(copyAction, &QAction::triggered, this, &GraphEditorWidget::copySelection);
|
||||||
|
m_view->addAction(copyAction);
|
||||||
|
|
||||||
|
auto *pasteAction = new QAction(QString("Paste Selection"), m_view);
|
||||||
|
pasteAction->setShortcut(QKeySequence::Paste);
|
||||||
|
pasteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(pasteAction, &QAction::triggered, [this]() {
|
||||||
|
pasteSelection(QPointF(30, 30));
|
||||||
|
});
|
||||||
|
m_view->addAction(pasteAction);
|
||||||
|
|
||||||
|
auto *duplicateAction = new QAction(QString("Duplicate Selection"), m_view);
|
||||||
|
duplicateAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
||||||
|
duplicateAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(duplicateAction, &QAction::triggered, this, &GraphEditorWidget::duplicateSelection);
|
||||||
|
m_view->addAction(duplicateAction);
|
||||||
|
|
||||||
auto *zoomFitAllAction = new QAction(QString("Zoom Fit All"), m_view);
|
auto *zoomFitAllAction = new QAction(QString("Zoom Fit All"), m_view);
|
||||||
connect(zoomFitAllAction, &QAction::triggered, m_view, &QtNodes::GraphicsView::zoomFitAll);
|
connect(zoomFitAllAction, &QAction::triggered, m_view, &QtNodes::GraphicsView::zoomFitAll);
|
||||||
m_view->addAction(zoomFitAllAction);
|
m_view->addAction(zoomFitAllAction);
|
||||||
|
|
@ -295,23 +472,33 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
||||||
m_view->addAction(loadPresetAction);
|
m_view->addAction(loadPresetAction);
|
||||||
|
|
||||||
auto *createVirtualSinkAction = new QAction(QString("Create Virtual Sink"), m_view);
|
auto *createVirtualSinkAction = new QAction(QString("Create Virtual Sink"), m_view);
|
||||||
|
createVirtualSinkAction->setShortcutContext(Qt::WidgetShortcut);
|
||||||
connect(createVirtualSinkAction, &QAction::triggered, [this]() {
|
connect(createVirtualSinkAction, &QAction::triggered, [this]() {
|
||||||
const int index = ++m_virtualSinkCount;
|
const int index = ++m_virtualSinkCount;
|
||||||
const QString name = QString("Potato_Virtual_Sink_%1").arg(index);
|
const QString name = QString("Potato_Virtual_Sink_%1").arg(index);
|
||||||
const QString description = QString("Virtual Sink %1").arg(index);
|
const QString description = QString("Virtual Sink %1").arg(index);
|
||||||
|
|
||||||
|
m_pendingPastePositions.insert(name, m_lastContextMenuPos);
|
||||||
|
|
||||||
if (!m_controller->createVirtualSink(name, description, 2, 48000)) {
|
if (!m_controller->createVirtualSink(name, description, 2, 48000)) {
|
||||||
qWarning() << "Failed to create virtual sink" << name;
|
qWarning() << "Failed to create virtual sink" << name;
|
||||||
|
m_pendingPastePositions.remove(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
m_view->addAction(createVirtualSinkAction);
|
m_view->addAction(createVirtualSinkAction);
|
||||||
|
|
||||||
auto *createVirtualSourceAction = new QAction(QString("Create Virtual Source"), m_view);
|
auto *createVirtualSourceAction = new QAction(QString("Create Virtual Source"), m_view);
|
||||||
|
createVirtualSourceAction->setShortcutContext(Qt::WidgetShortcut);
|
||||||
connect(createVirtualSourceAction, &QAction::triggered, [this]() {
|
connect(createVirtualSourceAction, &QAction::triggered, [this]() {
|
||||||
const int index = ++m_virtualSourceCount;
|
const int index = ++m_virtualSourceCount;
|
||||||
const QString name = QString("Potato_Virtual_Source_%1").arg(index);
|
const QString name = QString("Potato_Virtual_Source_%1").arg(index);
|
||||||
const QString description = QString("Virtual Source %1").arg(index);
|
const QString description = QString("Virtual Source %1").arg(index);
|
||||||
|
|
||||||
|
m_pendingPastePositions.insert(name, m_lastContextMenuPos);
|
||||||
|
|
||||||
if (!m_controller->createVirtualSource(name, description, 2, 48000)) {
|
if (!m_controller->createVirtualSource(name, description, 2, 48000)) {
|
||||||
qWarning() << "Failed to create virtual source" << name;
|
qWarning() << "Failed to create virtual source" << name;
|
||||||
|
m_pendingPastePositions.remove(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
m_view->addAction(createVirtualSourceAction);
|
m_view->addAction(createVirtualSourceAction);
|
||||||
|
|
@ -392,6 +579,9 @@ void GraphEditorWidget::refreshGraph()
|
||||||
m_linkIdToConnection.clear();
|
m_linkIdToConnection.clear();
|
||||||
m_nodeLinkCounts.clear();
|
m_nodeLinkCounts.clear();
|
||||||
m_linksById.clear();
|
m_linksById.clear();
|
||||||
|
m_pendingPastePositions.clear();
|
||||||
|
m_pendingPasteVolumes.clear();
|
||||||
|
m_pendingPasteLinks.clear();
|
||||||
|
|
||||||
m_model->reset();
|
m_model->reset();
|
||||||
syncGraph();
|
syncGraph();
|
||||||
|
|
@ -403,9 +593,21 @@ void GraphEditorWidget::refreshGraph()
|
||||||
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
||||||
{
|
{
|
||||||
if (isAudioEndpoint(node)) {
|
if (isAudioEndpoint(node)) {
|
||||||
m_model->addPipeWireNode(node);
|
const QtNodes::NodeId nodeId = m_model->addPipeWireNode(node);
|
||||||
refreshNodeMeter(node.id, node);
|
refreshNodeMeter(node.id, node);
|
||||||
refreshMixerStrip(node.id, node);
|
refreshMixerStrip(node.id, node);
|
||||||
|
if (m_pendingPastePositions.contains(node.stableId)) {
|
||||||
|
const QPointF pos = m_pendingPastePositions.take(node.stableId);
|
||||||
|
m_model->setNodeData(nodeId, QtNodes::NodeRole::Position, pos);
|
||||||
|
updateLayoutState();
|
||||||
|
}
|
||||||
|
if (m_pendingPasteVolumes.contains(node.stableId)) {
|
||||||
|
const NodeVolumeState state = m_pendingPasteVolumes.take(node.stableId);
|
||||||
|
m_controller->setNodeVolume(node.id, state.volume, state.mute);
|
||||||
|
m_model->setNodeVolumeState(node.id, state, false);
|
||||||
|
updateMixerState(node.id, node);
|
||||||
|
}
|
||||||
|
tryResolvePendingLinks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,6 +625,7 @@ void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node)
|
||||||
updateNodeMeterState(node.id, node);
|
updateNodeMeterState(node.id, node);
|
||||||
refreshMixerStrip(node.id, node);
|
refreshMixerStrip(node.id, node);
|
||||||
updateMixerState(node.id, node);
|
updateMixerState(node.id, node);
|
||||||
|
tryResolvePendingLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
||||||
|
|
@ -526,18 +729,60 @@ void GraphEditorWidget::onConnectionCreated(QtNodes::ConnectionId const connecti
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionId.outPortIndex >= static_cast<QtNodes::PortIndex>(outInfo->outputPorts.size())) {
|
if (connectionId.outPortIndex >= static_cast<QtNodes::PortIndex>(outInfo->outputPorts.size())) {
|
||||||
|
qWarning() << "Output port index out of bounds:" << connectionId.outPortIndex << ">=" << outInfo->outputPorts.size();
|
||||||
|
m_model->deleteConnection(connectionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionId.inPortIndex >= static_cast<QtNodes::PortIndex>(inInfo->inputPorts.size())) {
|
if (connectionId.inPortIndex >= static_cast<QtNodes::PortIndex>(inInfo->inputPorts.size())) {
|
||||||
|
qWarning() << "Input port index out of bounds:" << connectionId.inPortIndex << ">=" << inInfo->inputPorts.size();
|
||||||
|
m_model->deleteConnection(connectionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t outputPortId = outInfo->outputPorts.at(connectionId.outPortIndex).id;
|
const uint32_t outputPortId = outInfo->outputPorts.at(connectionId.outPortIndex).id;
|
||||||
const uint32_t inputPortId = inInfo->inputPorts.at(connectionId.inPortIndex).id;
|
const uint32_t inputPortId = inInfo->inputPorts.at(connectionId.inPortIndex).id;
|
||||||
|
|
||||||
|
const Potato::NodeInfo freshOutInfo = m_controller->nodeById(outInfo->id);
|
||||||
|
const Potato::NodeInfo freshInInfo = m_controller->nodeById(inInfo->id);
|
||||||
|
|
||||||
|
if (!freshOutInfo.isValid() || !freshInInfo.isValid()) {
|
||||||
|
qWarning() << "Node no longer exists in PipeWire";
|
||||||
|
m_model->deleteConnection(connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool outputPortExists = false;
|
||||||
|
for (const auto &port : freshOutInfo.outputPorts) {
|
||||||
|
if (port.id == outputPortId) {
|
||||||
|
outputPortExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inputPortExists = false;
|
||||||
|
for (const auto &port : freshInInfo.inputPorts) {
|
||||||
|
if (port.id == inputPortId) {
|
||||||
|
inputPortExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputPortExists) {
|
||||||
|
qWarning() << "Output port" << outputPortId << "does not exist in PipeWire node" << outInfo->id;
|
||||||
|
m_model->deleteConnection(connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputPortExists) {
|
||||||
|
qWarning() << "Input port" << inputPortId << "does not exist in PipeWire node" << inInfo->id;
|
||||||
|
m_model->deleteConnection(connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const uint32_t linkId = m_controller->createLink(outInfo->id, outputPortId, inInfo->id, inputPortId);
|
const uint32_t linkId = m_controller->createLink(outInfo->id, outputPortId, inInfo->id, inputPortId);
|
||||||
if (linkId == 0) {
|
if (linkId == 0) {
|
||||||
|
qWarning() << "Failed to create link between" << outInfo->id << ":" << outputPortId << "->" << inInfo->id << ":" << inputPortId;
|
||||||
m_model->deleteConnection(connectionId);
|
m_model->deleteConnection(connectionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -813,12 +1058,18 @@ void GraphEditorWidget::updateNodeMeterLabel(QLabel *label)
|
||||||
|
|
||||||
bool GraphEditorWidget::eventFilter(QObject *object, QEvent *event)
|
bool GraphEditorWidget::eventFilter(QObject *object, QEvent *event)
|
||||||
{
|
{
|
||||||
if (auto *label = qobject_cast<QLabel*>(object)) {
|
if (object == m_view->viewport()) {
|
||||||
if (event->type() == QEvent::Resize || event->type() == QEvent::Show) {
|
if (event->type() == QEvent::MouseButtonPress) {
|
||||||
updateNodeMeterLabel(label);
|
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
|
||||||
|
if (mouseEvent->button() == Qt::MiddleButton) {
|
||||||
|
m_view->centerOn(m_view->mapToScene(mouseEvent->pos()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (event->type() == QEvent::ContextMenu) {
|
||||||
|
QContextMenuEvent *contextEvent = static_cast<QContextMenuEvent*>(event);
|
||||||
|
m_lastContextMenuPos = m_view->mapToScene(contextEvent->pos());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return QWidget::eventFilter(object, event);
|
return QWidget::eventFilter(object, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1061,3 +1312,320 @@ void GraphEditorWidget::pushVolumeCommand(uint32_t nodeId, const NodeVolumeState
|
||||||
}
|
}
|
||||||
m_scene->undoStack().push(new VolumeChangeCommand(this, nodeId, previous, next));
|
m_scene->undoStack().push(new VolumeChangeCommand(this, nodeId, previous, next));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::removeDefaultCopyPasteActions()
|
||||||
|
{
|
||||||
|
const QList<QAction*> actions = m_view->actions();
|
||||||
|
for (QAction *action : actions) {
|
||||||
|
const QString text = action->text();
|
||||||
|
if (text.contains("Copy Selection")
|
||||||
|
|| text.contains("Paste Selection")
|
||||||
|
|| text.contains("Duplicate Selection")
|
||||||
|
|| text.contains("Delete Selection")) {
|
||||||
|
m_view->removeAction(action);
|
||||||
|
action->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GraphEditorWidget::mediaClassToString(Potato::MediaClass mediaClass) const
|
||||||
|
{
|
||||||
|
switch (mediaClass) {
|
||||||
|
case Potato::MediaClass::AudioSink:
|
||||||
|
return QString("Audio/Sink");
|
||||||
|
case Potato::MediaClass::AudioSource:
|
||||||
|
return QString("Audio/Source");
|
||||||
|
case Potato::MediaClass::AudioDuplex:
|
||||||
|
return QString("Audio/Duplex");
|
||||||
|
case Potato::MediaClass::Stream:
|
||||||
|
return QString("Stream");
|
||||||
|
default:
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Potato::MediaClass GraphEditorWidget::mediaClassFromString(const QString &value) const
|
||||||
|
{
|
||||||
|
if (value.contains("Audio/Source")) {
|
||||||
|
return Potato::MediaClass::AudioSource;
|
||||||
|
}
|
||||||
|
if (value.contains("Audio/Duplex")) {
|
||||||
|
return Potato::MediaClass::AudioDuplex;
|
||||||
|
}
|
||||||
|
if (value.contains("Audio/Sink")) {
|
||||||
|
return Potato::MediaClass::AudioSink;
|
||||||
|
}
|
||||||
|
if (value.contains("Stream")) {
|
||||||
|
return Potato::MediaClass::Stream;
|
||||||
|
}
|
||||||
|
return Potato::MediaClass::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GraphEditorWidget::findNodeByStableId(const QString &stableId, Potato::NodeInfo &node) const
|
||||||
|
{
|
||||||
|
const QVector<Potato::NodeInfo> nodes = m_controller->nodes();
|
||||||
|
for (const auto &item : nodes) {
|
||||||
|
if (item.stableId == stableId) {
|
||||||
|
node = item;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GraphEditorWidget::findPortByName(const Potato::NodeInfo &node, const QString &name, bool output, uint32_t &portId) const
|
||||||
|
{
|
||||||
|
const auto &ports = output ? node.outputPorts : node.inputPorts;
|
||||||
|
for (const auto &port : ports) {
|
||||||
|
if (port.name == name) {
|
||||||
|
portId = port.id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::copySelection()
|
||||||
|
{
|
||||||
|
QJsonArray nodesJson;
|
||||||
|
QSet<QString> selectedStableIds;
|
||||||
|
QPointF sum;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
const QList<QGraphicsItem*> items = m_scene->selectedItems();
|
||||||
|
for (auto *item : items) {
|
||||||
|
if (auto *nodeObj = qgraphicsitem_cast<QtNodes::NodeGraphicsObject*>(item)) {
|
||||||
|
const Potato::NodeInfo *info = m_model->nodeInfo(nodeObj->nodeId());
|
||||||
|
if (!info || info->type != Potato::NodeType::Virtual || info->stableId.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QJsonObject nodeJson;
|
||||||
|
nodeJson["stable_id"] = info->stableId;
|
||||||
|
nodeJson["name"] = info->name;
|
||||||
|
nodeJson["description"] = info->description;
|
||||||
|
nodeJson["media_class"] = mediaClassToString(info->mediaClass);
|
||||||
|
const int channels = std::max(info->inputPorts.size(), info->outputPorts.size());
|
||||||
|
nodeJson["channels"] = channels > 0 ? channels : 2;
|
||||||
|
nodeJson["rate"] = 48000;
|
||||||
|
const QPointF pos = m_model->nodeData(nodeObj->nodeId(), QtNodes::NodeRole::Position).toPointF();
|
||||||
|
nodeJson["x"] = pos.x();
|
||||||
|
nodeJson["y"] = pos.y();
|
||||||
|
NodeVolumeState state;
|
||||||
|
if (m_model->nodeVolumeState(info->id, state)) {
|
||||||
|
nodeJson["volume"] = state.volume;
|
||||||
|
nodeJson["mute"] = state.mute;
|
||||||
|
}
|
||||||
|
nodesJson.append(nodeJson);
|
||||||
|
selectedStableIds.insert(info->stableId);
|
||||||
|
sum += pos;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodesJson.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray linksJson;
|
||||||
|
const QVector<Potato::LinkInfo> links = m_controller->links();
|
||||||
|
for (const auto &link : links) {
|
||||||
|
const Potato::NodeInfo outNode = m_controller->nodeById(link.outputNodeId);
|
||||||
|
const Potato::NodeInfo inNode = m_controller->nodeById(link.inputNodeId);
|
||||||
|
if (!selectedStableIds.contains(outNode.stableId) || !selectedStableIds.contains(inNode.stableId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString outPortName;
|
||||||
|
QString inPortName;
|
||||||
|
for (const auto &port : outNode.outputPorts) {
|
||||||
|
if (port.id == link.outputPortId) {
|
||||||
|
outPortName = port.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &port : inNode.inputPorts) {
|
||||||
|
if (port.id == link.inputPortId) {
|
||||||
|
inPortName = port.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outPortName.isEmpty() || inPortName.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QJsonObject linkJson;
|
||||||
|
linkJson["source"] = QString("%1:%2").arg(outNode.stableId, outPortName);
|
||||||
|
linkJson["target"] = QString("%1:%2").arg(inNode.stableId, inPortName);
|
||||||
|
linksJson.append(linkJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF center = count > 0 ? sum / count : QPointF(0, 0);
|
||||||
|
QJsonObject root;
|
||||||
|
root["nodes"] = nodesJson;
|
||||||
|
root["links"] = linksJson;
|
||||||
|
root["center_x"] = center.x();
|
||||||
|
root["center_y"] = center.y();
|
||||||
|
root["version"] = QString("1.0");
|
||||||
|
m_clipboardJson = root;
|
||||||
|
|
||||||
|
QJsonDocument doc(root);
|
||||||
|
auto *mime = new QMimeData();
|
||||||
|
mime->setData("application/potato-virtual-graph", doc.toJson(QJsonDocument::Compact));
|
||||||
|
mime->setText(doc.toJson(QJsonDocument::Compact));
|
||||||
|
QGuiApplication::clipboard()->setMimeData(mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::pasteSelection(const QPointF &offset)
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
const QMimeData *mime = QGuiApplication::clipboard()->mimeData();
|
||||||
|
if (mime && mime->hasFormat("application/potato-virtual-graph")) {
|
||||||
|
root = QJsonDocument::fromJson(mime->data("application/potato-virtual-graph")).object();
|
||||||
|
} else if (!m_clipboardJson.isEmpty()) {
|
||||||
|
root = m_clipboardJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QString, QString> stableMap;
|
||||||
|
QSet<QString> existingNames;
|
||||||
|
for (const auto &node : m_controller->nodes()) {
|
||||||
|
existingNames.insert(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray nodesJson = root.value("nodes").toArray();
|
||||||
|
for (const auto &entry : nodesJson) {
|
||||||
|
const QJsonObject nodeObj = entry.toObject();
|
||||||
|
const QString baseName = nodeObj.value("name").toString();
|
||||||
|
const QString description = nodeObj.value("description").toString();
|
||||||
|
const QString stableId = nodeObj.value("stable_id").toString();
|
||||||
|
const QString mediaClassValue = nodeObj.value("media_class").toString();
|
||||||
|
if (baseName.isEmpty() || stableId.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString newName = baseName + QString(" Copy");
|
||||||
|
int suffix = 2;
|
||||||
|
while (existingNames.contains(newName)) {
|
||||||
|
newName = baseName + QString(" Copy %1").arg(suffix++);
|
||||||
|
}
|
||||||
|
existingNames.insert(newName);
|
||||||
|
stableMap.insert(stableId, newName);
|
||||||
|
|
||||||
|
const int channels = nodeObj.value("channels").toInt(2);
|
||||||
|
const int rate = nodeObj.value("rate").toInt(48000);
|
||||||
|
const Potato::MediaClass mediaClass = mediaClassFromString(mediaClassValue);
|
||||||
|
const QString newDescription = description.isEmpty() ? newName : description;
|
||||||
|
|
||||||
|
if (mediaClass == Potato::MediaClass::AudioSource) {
|
||||||
|
m_controller->createVirtualSource(newName, newDescription, channels, rate);
|
||||||
|
} else {
|
||||||
|
m_controller->createVirtualSink(newName, newDescription, channels, rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF pos(nodeObj.value("x").toDouble(), nodeObj.value("y").toDouble());
|
||||||
|
m_pendingPastePositions.insert(newName, pos + offset);
|
||||||
|
const float volume = static_cast<float>(nodeObj.value("volume").toDouble(1.0));
|
||||||
|
const bool mute = nodeObj.value("mute").toBool(false);
|
||||||
|
m_pendingPasteVolumes.insert(newName, NodeVolumeState{volume, mute});
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray linksJson = root.value("links").toArray();
|
||||||
|
for (const auto &entry : linksJson) {
|
||||||
|
const QJsonObject linkObj = entry.toObject();
|
||||||
|
const QString source = linkObj.value("source").toString();
|
||||||
|
const QString target = linkObj.value("target").toString();
|
||||||
|
const int splitSource = source.lastIndexOf(':');
|
||||||
|
const int splitTarget = target.lastIndexOf(':');
|
||||||
|
if (splitSource <= 0 || splitTarget <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString outStable = source.left(splitSource);
|
||||||
|
const QString outPort = source.mid(splitSource + 1);
|
||||||
|
const QString inStable = target.left(splitTarget);
|
||||||
|
const QString inPort = target.mid(splitTarget + 1);
|
||||||
|
if (!stableMap.contains(outStable) || !stableMap.contains(inStable)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PendingPasteLink link;
|
||||||
|
link.outStableId = stableMap.value(outStable);
|
||||||
|
link.outPortName = outPort;
|
||||||
|
link.inStableId = stableMap.value(inStable);
|
||||||
|
link.inPortName = inPort;
|
||||||
|
m_pendingPasteLinks.append(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryResolvePendingLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::duplicateSelection()
|
||||||
|
{
|
||||||
|
copySelection();
|
||||||
|
pasteSelection(QPointF(40, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::deleteSelection()
|
||||||
|
{
|
||||||
|
if (!m_scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<QGraphicsItem*> items = m_scene->selectedItems();
|
||||||
|
QList<QtNodes::NodeId> virtualNodeIds;
|
||||||
|
QList<QtNodes::NodeId> otherNodeIds;
|
||||||
|
|
||||||
|
for (auto *item : items) {
|
||||||
|
if (auto *nodeObj = qgraphicsitem_cast<QtNodes::NodeGraphicsObject*>(item)) {
|
||||||
|
const Potato::NodeInfo *info = m_model->nodeInfo(nodeObj->nodeId());
|
||||||
|
if (info && info->type == Potato::NodeType::Virtual) {
|
||||||
|
virtualNodeIds.append(nodeObj->nodeId());
|
||||||
|
} else if (info) {
|
||||||
|
otherNodeIds.append(nodeObj->nodeId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!virtualNodeIds.isEmpty()) {
|
||||||
|
m_scene->undoStack().push(new DeleteVirtualNodeCommand(this, m_scene, virtualNodeIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!otherNodeIds.isEmpty()) {
|
||||||
|
m_scene->clearSelection();
|
||||||
|
for (const auto nodeId : otherNodeIds) {
|
||||||
|
if (const auto *nodeObj = m_scene->nodeGraphicsObject(nodeId)) {
|
||||||
|
const_cast<QtNodes::NodeGraphicsObject*>(nodeObj)->setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_scene->undoStack().push(new QtNodes::DeleteCommand(m_scene));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (virtualNodeIds.isEmpty() && otherNodeIds.isEmpty() && !items.isEmpty()) {
|
||||||
|
m_scene->undoStack().push(new QtNodes::DeleteCommand(m_scene));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphEditorWidget::tryResolvePendingLinks()
|
||||||
|
{
|
||||||
|
if (m_pendingPasteLinks.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<PendingPasteLink> remaining;
|
||||||
|
for (const auto &pending : m_pendingPasteLinks) {
|
||||||
|
Potato::NodeInfo outNode;
|
||||||
|
Potato::NodeInfo inNode;
|
||||||
|
if (!findNodeByStableId(pending.outStableId, outNode)
|
||||||
|
|| !findNodeByStableId(pending.inStableId, inNode)) {
|
||||||
|
remaining.append(pending);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t outPortId = 0;
|
||||||
|
uint32_t inPortId = 0;
|
||||||
|
if (!findPortByName(outNode, pending.outPortName, true, outPortId)
|
||||||
|
|| !findPortByName(inNode, pending.inPortName, false, inPortId)) {
|
||||||
|
remaining.append(pending);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_controller->createLink(outNode.id, outPortId, inNode.id, inPortId);
|
||||||
|
}
|
||||||
|
m_pendingPasteLinks = remaining;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
class AudioLevelMeter;
|
class AudioLevelMeter;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
|
@ -24,12 +27,14 @@ class QSplitter;
|
||||||
class QTabWidget;
|
class QTabWidget;
|
||||||
class PresetManager;
|
class PresetManager;
|
||||||
class VolumeChangeCommand;
|
class VolumeChangeCommand;
|
||||||
|
class DeleteVirtualNodeCommand;
|
||||||
|
|
||||||
class GraphEditorWidget : public QWidget
|
class GraphEditorWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
friend class VolumeChangeCommand;
|
friend class VolumeChangeCommand;
|
||||||
|
friend class DeleteVirtualNodeCommand;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent = nullptr);
|
explicit GraphEditorWidget(Potato::PipeWireController *controller, QWidget *parent = nullptr);
|
||||||
|
|
@ -57,6 +62,16 @@ private:
|
||||||
void applySoloState();
|
void applySoloState();
|
||||||
void applyVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool updateMixer);
|
void applyVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool updateMixer);
|
||||||
void pushVolumeCommand(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState &next);
|
void pushVolumeCommand(uint32_t nodeId, const NodeVolumeState &previous, const NodeVolumeState &next);
|
||||||
|
void copySelection();
|
||||||
|
void pasteSelection(const QPointF &offset);
|
||||||
|
void duplicateSelection();
|
||||||
|
void deleteSelection();
|
||||||
|
void tryResolvePendingLinks();
|
||||||
|
bool findNodeByStableId(const QString &stableId, Potato::NodeInfo &node) const;
|
||||||
|
bool findPortByName(const Potato::NodeInfo &node, const QString &name, bool output, uint32_t &portId) const;
|
||||||
|
QString mediaClassToString(Potato::MediaClass mediaClass) const;
|
||||||
|
Potato::MediaClass mediaClassFromString(const QString &value) const;
|
||||||
|
void removeDefaultCopyPasteActions();
|
||||||
void handleLinkRemoved(uint32_t linkId);
|
void handleLinkRemoved(uint32_t linkId);
|
||||||
bool isMeterNode(uint32_t nodeId) const;
|
bool isMeterNode(uint32_t nodeId) const;
|
||||||
int activeLinkCount(uint32_t nodeId) const;
|
int activeLinkCount(uint32_t nodeId) const;
|
||||||
|
|
@ -65,6 +80,13 @@ private:
|
||||||
bool eventFilter(QObject *object, QEvent *event) override;
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
|
||||||
Potato::PipeWireController *m_controller = nullptr;
|
Potato::PipeWireController *m_controller = nullptr;
|
||||||
|
struct PendingPasteLink {
|
||||||
|
QString outStableId;
|
||||||
|
QString outPortName;
|
||||||
|
QString inStableId;
|
||||||
|
QString inPortName;
|
||||||
|
};
|
||||||
|
|
||||||
PipeWireGraphModel *m_model = nullptr;
|
PipeWireGraphModel *m_model = nullptr;
|
||||||
QtNodes::BasicGraphicsScene *m_scene = nullptr;
|
QtNodes::BasicGraphicsScene *m_scene = nullptr;
|
||||||
QtNodes::GraphicsView *m_view = nullptr;
|
QtNodes::GraphicsView *m_view = nullptr;
|
||||||
|
|
@ -83,6 +105,10 @@ private:
|
||||||
QMap<uint32_t, NodeVolumeState> m_mixerStartState;
|
QMap<uint32_t, NodeVolumeState> m_mixerStartState;
|
||||||
QMap<uint32_t, NodeVolumeState> m_mixerLastState;
|
QMap<uint32_t, NodeVolumeState> m_mixerLastState;
|
||||||
QSet<uint32_t> m_mixerSoloNodes;
|
QSet<uint32_t> m_mixerSoloNodes;
|
||||||
|
QJsonObject m_clipboardJson;
|
||||||
|
QHash<QString, QPointF> m_pendingPastePositions;
|
||||||
|
QHash<QString, NodeVolumeState> m_pendingPasteVolumes;
|
||||||
|
QVector<PendingPasteLink> m_pendingPasteLinks;
|
||||||
|
|
||||||
QSet<QString> m_ignoreCreate;
|
QSet<QString> m_ignoreCreate;
|
||||||
QSet<QString> m_ignoreDelete;
|
QSet<QString> m_ignoreDelete;
|
||||||
|
|
@ -108,4 +134,5 @@ private:
|
||||||
int m_meterProfileFrames = 0;
|
int m_meterProfileFrames = 0;
|
||||||
PresetManager *m_presetManager = nullptr;
|
PresetManager *m_presetManager = nullptr;
|
||||||
bool m_ignoreVolumeUndo = false;
|
bool m_ignoreVolumeUndo = false;
|
||||||
|
QPointF m_lastContextMenuPos;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,16 @@ bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Potato::NodeInfo info = m_nodes.at(nodeId);
|
||||||
|
if (info.type != Potato::NodeType::Virtual) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Potato::NodeInfo liveNode = m_controller ? m_controller->nodeById(info.id) : Potato::NodeInfo{};
|
||||||
|
if (liveNode.isValid()) {
|
||||||
|
m_controller->destroyVirtualNode(info.id);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<QtNodes::ConnectionId> toRemove;
|
std::vector<QtNodes::ConnectionId> toRemove;
|
||||||
for (const auto &conn : m_connections) {
|
for (const auto &conn : m_connections) {
|
||||||
if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) {
|
if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) {
|
||||||
|
|
@ -517,9 +527,16 @@ bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject PipeWireGraphModel::saveNode(QtNodes::NodeId const) const
|
QJsonObject PipeWireGraphModel::saveNode(QtNodes::NodeId const nodeId) const
|
||||||
{
|
{
|
||||||
return QJsonObject();
|
QJsonObject obj;
|
||||||
|
obj["id"] = static_cast<qint64>(nodeId);
|
||||||
|
const QPointF pos = nodeData(nodeId, QtNodes::NodeRole::Position).toPointF();
|
||||||
|
QJsonObject posJson;
|
||||||
|
posJson["x"] = pos.x();
|
||||||
|
posJson["y"] = pos.y();
|
||||||
|
obj["position"] = posJson;
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PipeWireGraphModel::loadNode(QJsonObject const &)
|
void PipeWireGraphModel::loadNode(QJsonObject const &)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <pipewire/pipewire.h>
|
#include <pipewire/pipewire.h>
|
||||||
#include <pipewire/keys.h>
|
#include <pipewire/keys.h>
|
||||||
#include <pipewire/properties.h>
|
#include <pipewire/properties.h>
|
||||||
|
#include <pipewire/proxy.h>
|
||||||
#include <pipewire/stream.h>
|
#include <pipewire/stream.h>
|
||||||
#include <pipewire/node.h>
|
#include <pipewire/node.h>
|
||||||
#include <spa/param/props.h>
|
#include <spa/param/props.h>
|
||||||
|
|
@ -557,6 +558,38 @@ bool PipeWireController::createVirtualSource(const QString &name, const QString
|
||||||
return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Source", channels, rate);
|
return createVirtualDevice(name, description, "support.null-audio-sink", "Audio/Source", channels, rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PipeWireController::destroyVirtualNode(uint32_t nodeId)
|
||||||
|
{
|
||||||
|
if (!m_threadLoop || !m_core || !m_registry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeInfo node = nodeById(nodeId);
|
||||||
|
if (!node.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type != NodeType::Virtual && !node.name.startsWith("Potato_")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock();
|
||||||
|
|
||||||
|
for (auto it = m_virtualDevices.begin(); it != m_virtualDevices.end(); ) {
|
||||||
|
struct pw_proxy *proxy = *it;
|
||||||
|
if (proxy && pw_proxy_get_bound_id(proxy) == nodeId) {
|
||||||
|
it = m_virtualDevices.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int result = pw_registry_destroy(m_registry, nodeId);
|
||||||
|
unlock();
|
||||||
|
|
||||||
|
return result == 0;
|
||||||
|
}
|
||||||
|
|
||||||
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
||||||
{
|
{
|
||||||
QMutexLocker lock(&m_meterMutex);
|
QMutexLocker lock(&m_meterMutex);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ public:
|
||||||
bool setNodeVolume(uint32_t nodeId, float volume, bool mute);
|
bool setNodeVolume(uint32_t nodeId, float volume, bool mute);
|
||||||
bool createVirtualSink(const QString &name, const QString &description, int channels, int rate);
|
bool createVirtualSink(const QString &name, const QString &description, int channels, int rate);
|
||||||
bool createVirtualSource(const QString &name, const QString &description, int channels, int rate);
|
bool createVirtualSource(const QString &name, const QString &description, int channels, int rate);
|
||||||
|
bool destroyVirtualNode(uint32_t nodeId);
|
||||||
|
|
||||||
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||||
uint32_t inputNodeId, uint32_t inputPortId);
|
uint32_t inputNodeId, uint32_t inputPortId);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue