Add delete
This commit is contained in:
parent
f2d0494af2
commit
05d6c06603
6 changed files with 429 additions and 6 deletions
|
|
@ -1240,9 +1240,9 @@ private:
|
|||
**Estimated Time:** 1-2 weeks
|
||||
- [x] Integrate QUndoStack for all graph operations
|
||||
- [x] Implement command classes for link, volume, node operations
|
||||
- [ ] Add keyboard shortcuts (Delete, Ctrl+D, Ctrl+Z, etc.)
|
||||
- [ ] Implement context menus for nodes/canvas
|
||||
- [ ] Add copy/paste/duplicate functionality
|
||||
- [x] Add keyboard shortcuts (Delete, Ctrl+D, Ctrl+Z, etc.)
|
||||
- [x] Implement context menus for nodes/canvas
|
||||
- [x] Add copy/paste/duplicate functionality
|
||||
- [ ] **Acceptance Criteria:** Full undo/redo history, keyboard shortcuts work
|
||||
|
||||
### Milestone 7: Error Handling & Edge Cases
|
||||
|
|
|
|||
|
|
@ -17,15 +17,24 @@
|
|||
#include <QTimer>
|
||||
#include <QUndoCommand>
|
||||
#include <QEvent>
|
||||
#include <QGraphicsItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QSizePolicy>
|
||||
#include <QElapsedTimer>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QMimeData>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QtNodes/GraphicsViewStyle>
|
||||
#include <QtNodes/NodeStyle>
|
||||
#include <QtNodes/internal/NodeGraphicsObject.hpp>
|
||||
#include <QtNodes/internal/ConnectionGraphicsObject.hpp>
|
||||
#include <QtNodes/internal/UndoCommands.hpp>
|
||||
|
||||
#include "gui/ClickSlider.h"
|
||||
|
||||
|
|
@ -106,6 +115,8 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
m_model->loadLayout();
|
||||
m_scene = new QtNodes::BasicGraphicsScene(*m_model, this);
|
||||
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_splitter = new QSplitter(this);
|
||||
|
|
@ -218,10 +229,38 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
|
|||
|
||||
m_view->setContextMenuPolicy(Qt::ActionsContextMenu);
|
||||
|
||||
removeDefaultCopyPasteActions();
|
||||
|
||||
auto *refreshAction = new QAction(QString("Refresh Graph"), m_view);
|
||||
connect(refreshAction, &QAction::triggered, this, &GraphEditorWidget::refreshGraph);
|
||||
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);
|
||||
connect(zoomFitAllAction, &QAction::triggered, m_view, &QtNodes::GraphicsView::zoomFitAll);
|
||||
m_view->addAction(zoomFitAllAction);
|
||||
|
|
@ -392,6 +431,9 @@ void GraphEditorWidget::refreshGraph()
|
|||
m_linkIdToConnection.clear();
|
||||
m_nodeLinkCounts.clear();
|
||||
m_linksById.clear();
|
||||
m_pendingPastePositions.clear();
|
||||
m_pendingPasteVolumes.clear();
|
||||
m_pendingPasteLinks.clear();
|
||||
|
||||
m_model->reset();
|
||||
syncGraph();
|
||||
|
|
@ -403,9 +445,21 @@ void GraphEditorWidget::refreshGraph()
|
|||
void GraphEditorWidget::onNodeAdded(const Potato::NodeInfo &node)
|
||||
{
|
||||
if (isAudioEndpoint(node)) {
|
||||
m_model->addPipeWireNode(node);
|
||||
const QtNodes::NodeId nodeId = m_model->addPipeWireNode(node);
|
||||
refreshNodeMeter(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 +477,7 @@ void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node)
|
|||
updateNodeMeterState(node.id, node);
|
||||
refreshMixerStrip(node.id, node);
|
||||
updateMixerState(node.id, node);
|
||||
tryResolvePendingLinks();
|
||||
}
|
||||
|
||||
void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
|
||||
|
|
@ -1061,3 +1116,287 @@ void GraphEditorWidget::pushVolumeCommand(uint32_t nodeId, const NodeVolumeState
|
|||
}
|
||||
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) {
|
||||
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 <QElapsedTimer>
|
||||
#include <cstdint>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QVector>
|
||||
|
||||
class AudioLevelMeter;
|
||||
class QLabel;
|
||||
|
|
@ -57,6 +60,16 @@ private:
|
|||
void applySoloState();
|
||||
void applyVolumeState(uint32_t nodeId, const NodeVolumeState &state, bool updateMixer);
|
||||
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);
|
||||
bool isMeterNode(uint32_t nodeId) const;
|
||||
int activeLinkCount(uint32_t nodeId) const;
|
||||
|
|
@ -65,6 +78,13 @@ private:
|
|||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
|
||||
Potato::PipeWireController *m_controller = nullptr;
|
||||
struct PendingPasteLink {
|
||||
QString outStableId;
|
||||
QString outPortName;
|
||||
QString inStableId;
|
||||
QString inPortName;
|
||||
};
|
||||
|
||||
PipeWireGraphModel *m_model = nullptr;
|
||||
QtNodes::BasicGraphicsScene *m_scene = nullptr;
|
||||
QtNodes::GraphicsView *m_view = nullptr;
|
||||
|
|
@ -83,6 +103,10 @@ private:
|
|||
QMap<uint32_t, NodeVolumeState> m_mixerStartState;
|
||||
QMap<uint32_t, NodeVolumeState> m_mixerLastState;
|
||||
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_ignoreDelete;
|
||||
|
|
|
|||
|
|
@ -498,6 +498,16 @@ bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId)
|
|||
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;
|
||||
for (const auto &conn : m_connections) {
|
||||
if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) {
|
||||
|
|
@ -517,9 +527,16 @@ bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId)
|
|||
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 &)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <pipewire/pipewire.h>
|
||||
#include <pipewire/keys.h>
|
||||
#include <pipewire/properties.h>
|
||||
#include <pipewire/proxy.h>
|
||||
#include <pipewire/stream.h>
|
||||
#include <pipewire/node.h>
|
||||
#include <spa/param/props.h>
|
||||
|
|
@ -557,6 +558,47 @@ bool PipeWireController::createVirtualSource(const QString &name, const QString
|
|||
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;
|
||||
}
|
||||
|
||||
bool destroyed = 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) {
|
||||
pw_proxy_destroy(proxy);
|
||||
it = m_virtualDevices.erase(it);
|
||||
destroyed = true;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (!destroyed) {
|
||||
auto *proxy = static_cast<struct pw_proxy*>(
|
||||
pw_registry_bind(m_registry, nodeId, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0));
|
||||
if (proxy) {
|
||||
pw_proxy_destroy(proxy);
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
float PipeWireController::nodeMeterPeak(uint32_t nodeId) const
|
||||
{
|
||||
QMutexLocker lock(&m_meterMutex);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ public:
|
|||
bool setNodeVolume(uint32_t nodeId, float volume, bool mute);
|
||||
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 destroyVirtualNode(uint32_t nodeId);
|
||||
|
||||
uint32_t createLink(uint32_t outputNodeId, uint32_t outputPortId,
|
||||
uint32_t inputNodeId, uint32_t inputPortId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue