Fix virtual nodes
This commit is contained in:
parent
9f33bcd017
commit
1dd4ef7327
7 changed files with 113 additions and 22 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QContextMenuEvent>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QGraphicsItem>
|
#include <QGraphicsItem>
|
||||||
|
|
@ -135,6 +136,9 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
|
||||||
}})");
|
}})");
|
||||||
|
|
||||||
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_view->viewport()->installEventFilter(this);
|
||||||
|
|
||||||
auto *layout = new QVBoxLayout(this);
|
auto *layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
@ -292,10 +296,21 @@ void GraphEditorWidget::captureDebugScreenshot(const QString &event) {
|
||||||
pixmap.save(m_debugScreenshotDir + "/" + filename);
|
pixmap.save(m_debugScreenshotDir + "/" + filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GraphEditorWidget::eventFilter(QObject *obj, QEvent *event) {
|
||||||
|
if (obj == m_view->viewport() &&
|
||||||
|
event->type() == QEvent::ContextMenu) {
|
||||||
|
auto *cme = static_cast<QContextMenuEvent *>(event);
|
||||||
|
m_lastContextMenuScenePos = m_view->mapToScene(cme->pos());
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
|
void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
|
||||||
QPointF scenePos = m_view->mapToScene(pos);
|
QPointF scenePos = m_view->mapToScene(pos);
|
||||||
|
m_lastContextMenuScenePos = scenePos;
|
||||||
|
|
||||||
uint32_t hitPwNodeId = 0;
|
uint32_t hitPwNodeId = 0;
|
||||||
|
QtNodes::NodeId hitQtNodeId = 0;
|
||||||
for (auto nodeId : m_model->allNodeIds()) {
|
for (auto nodeId : m_model->allNodeIds()) {
|
||||||
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
const WarpNodeData *data = m_model->warpNodeData(nodeId);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|
@ -308,13 +323,14 @@ void GraphEditorWidget::onContextMenuRequested(const QPoint &pos) {
|
||||||
QRectF nodeRect(nodePos, QSizeF(nodeSize));
|
QRectF nodeRect(nodePos, QSizeF(nodeSize));
|
||||||
if (nodeRect.contains(scenePos)) {
|
if (nodeRect.contains(scenePos)) {
|
||||||
hitPwNodeId = data->info.id.value;
|
hitPwNodeId = data->info.id.value;
|
||||||
|
hitQtNodeId = nodeId;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QPoint screenPos = m_view->mapToGlobal(pos);
|
QPoint screenPos = m_view->mapToGlobal(pos);
|
||||||
if (hitPwNodeId != 0) {
|
if (hitPwNodeId != 0) {
|
||||||
showNodeContextMenu(screenPos, hitPwNodeId);
|
showNodeContextMenu(screenPos, hitPwNodeId, hitQtNodeId);
|
||||||
} else {
|
} else {
|
||||||
showCanvasContextMenu(screenPos, scenePos);
|
showCanvasContextMenu(screenPos, scenePos);
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +343,14 @@ void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos,
|
||||||
QAction *createSource =
|
QAction *createSource =
|
||||||
menu.addAction(QStringLiteral("Create Virtual Source"));
|
menu.addAction(QStringLiteral("Create Virtual Source"));
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
|
||||||
|
pasteAction->setShortcut(QKeySequence::Paste);
|
||||||
|
pasteAction->setEnabled(!m_clipboardJson.isEmpty() ||
|
||||||
|
(QGuiApplication::clipboard()->mimeData() &&
|
||||||
|
QGuiApplication::clipboard()->mimeData()->hasFormat(
|
||||||
|
QStringLiteral(
|
||||||
|
"application/warppipe-virtual-graph"))));
|
||||||
|
menu.addSeparator();
|
||||||
QAction *autoArrange = menu.addAction(QStringLiteral("Auto-Arrange"));
|
QAction *autoArrange = menu.addAction(QStringLiteral("Auto-Arrange"));
|
||||||
|
|
||||||
QAction *chosen = menu.exec(screenPos);
|
QAction *chosen = menu.exec(screenPos);
|
||||||
|
|
@ -334,15 +358,17 @@ void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos,
|
||||||
createVirtualNode(true, scenePos);
|
createVirtualNode(true, scenePos);
|
||||||
} else if (chosen == createSource) {
|
} else if (chosen == createSource) {
|
||||||
createVirtualNode(false, scenePos);
|
createVirtualNode(false, scenePos);
|
||||||
|
} else if (chosen == pasteAction) {
|
||||||
|
pasteSelection(QPointF(0, 0));
|
||||||
} else if (chosen == autoArrange) {
|
} else if (chosen == autoArrange) {
|
||||||
m_model->autoArrange();
|
m_model->autoArrange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||||
uint32_t pwNodeId) {
|
uint32_t pwNodeId,
|
||||||
QtNodes::NodeId qtId = m_model->qtNodeIdForPw(pwNodeId);
|
QtNodes::NodeId qtNodeId) {
|
||||||
const WarpNodeData *data = m_model->warpNodeData(qtId);
|
const WarpNodeData *data = m_model->warpNodeData(qtNodeId);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -351,17 +377,42 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
|
||||||
bool isVirtual =
|
bool isVirtual =
|
||||||
type == WarpNodeType::kVirtualSink || type == WarpNodeType::kVirtualSource;
|
type == WarpNodeType::kVirtualSink || type == WarpNodeType::kVirtualSource;
|
||||||
|
|
||||||
if (!isVirtual) {
|
QMenu menu;
|
||||||
return;
|
|
||||||
|
QAction *copyAction = nullptr;
|
||||||
|
QAction *duplicateAction = nullptr;
|
||||||
|
QAction *deleteAction = nullptr;
|
||||||
|
|
||||||
|
if (isVirtual) {
|
||||||
|
copyAction = menu.addAction(QStringLiteral("Copy"));
|
||||||
|
copyAction->setShortcut(QKeySequence::Copy);
|
||||||
|
duplicateAction = menu.addAction(QStringLiteral("Duplicate"));
|
||||||
|
duplicateAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
||||||
|
menu.addSeparator();
|
||||||
|
deleteAction = menu.addAction(QStringLiteral("Delete"));
|
||||||
|
deleteAction->setShortcut(QKeySequence::Delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenu menu;
|
QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
|
||||||
QAction *deleteAction = menu.addAction(QStringLiteral("Delete Node"));
|
pasteAction->setShortcut(QKeySequence::Paste);
|
||||||
|
pasteAction->setEnabled(!m_clipboardJson.isEmpty() ||
|
||||||
|
(QGuiApplication::clipboard()->mimeData() &&
|
||||||
|
QGuiApplication::clipboard()->mimeData()->hasFormat(
|
||||||
|
QStringLiteral(
|
||||||
|
"application/warppipe-virtual-graph"))));
|
||||||
|
|
||||||
QAction *chosen = menu.exec(screenPos);
|
QAction *chosen = menu.exec(screenPos);
|
||||||
if (chosen == deleteAction && m_client) {
|
if (!chosen) {
|
||||||
m_client->RemoveNode(warppipe::NodeId{pwNodeId});
|
return;
|
||||||
m_model->refreshFromClient();
|
}
|
||||||
|
if (chosen == copyAction) {
|
||||||
|
copySelection();
|
||||||
|
} else if (chosen == duplicateAction) {
|
||||||
|
duplicateSelection();
|
||||||
|
} else if (chosen == deleteAction && m_client) {
|
||||||
|
deleteSelection();
|
||||||
|
} else if (chosen == pasteAction) {
|
||||||
|
pasteSelection(QPointF(0, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include <warppipe/warppipe.hpp>
|
#include <warppipe/warppipe.hpp>
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QPointF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace QtNodes {
|
namespace QtNodes {
|
||||||
|
using NodeId = unsigned int;
|
||||||
class BasicGraphicsScene;
|
class BasicGraphicsScene;
|
||||||
class GraphicsView;
|
class GraphicsView;
|
||||||
} // namespace QtNodes
|
} // namespace QtNodes
|
||||||
|
|
@ -33,6 +35,8 @@ public:
|
||||||
|
|
||||||
void setDebugScreenshotDir(const QString &dir);
|
void setDebugScreenshotDir(const QString &dir);
|
||||||
|
|
||||||
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void graphReady();
|
void graphReady();
|
||||||
|
|
||||||
|
|
@ -43,7 +47,8 @@ private slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void showCanvasContextMenu(const QPoint &screenPos, const QPointF &scenePos);
|
void showCanvasContextMenu(const QPoint &screenPos, const QPointF &scenePos);
|
||||||
void showNodeContextMenu(const QPoint &screenPos, uint32_t pwNodeId);
|
void showNodeContextMenu(const QPoint &screenPos, uint32_t pwNodeId,
|
||||||
|
QtNodes::NodeId qtNodeId);
|
||||||
void createVirtualNode(bool isSink, const QPointF &scenePos);
|
void createVirtualNode(bool isSink, const QPointF &scenePos);
|
||||||
void captureDebugScreenshot(const QString &event);
|
void captureDebugScreenshot(const QString &event);
|
||||||
|
|
||||||
|
|
@ -72,4 +77,5 @@ private:
|
||||||
bool m_graphReady = false;
|
bool m_graphReady = false;
|
||||||
QJsonObject m_clipboardJson;
|
QJsonObject m_clipboardJson;
|
||||||
std::vector<PendingPasteLink> m_pendingPasteLinks;
|
std::vector<PendingPasteLink> m_pendingPasteLinks;
|
||||||
|
QPointF m_lastContextMenuScenePos;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -628,14 +628,13 @@ WarpNodeType
|
||||||
WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) {
|
WarpGraphModel::classifyNode(const warppipe::NodeInfo &info) {
|
||||||
const std::string &mc = info.media_class;
|
const std::string &mc = info.media_class;
|
||||||
|
|
||||||
bool isVirtual = (info.name.find("warppipe") != std::string::npos);
|
|
||||||
|
|
||||||
if (mc == "Audio/Sink" || mc == "Audio/Duplex") {
|
if (mc == "Audio/Sink" || mc == "Audio/Duplex") {
|
||||||
return isVirtual ? WarpNodeType::kVirtualSink : WarpNodeType::kHardwareSink;
|
return info.is_virtual ? WarpNodeType::kVirtualSink
|
||||||
|
: WarpNodeType::kHardwareSink;
|
||||||
}
|
}
|
||||||
if (mc == "Audio/Source") {
|
if (mc == "Audio/Source") {
|
||||||
return isVirtual ? WarpNodeType::kVirtualSource
|
return info.is_virtual ? WarpNodeType::kVirtualSource
|
||||||
: WarpNodeType::kHardwareSource;
|
: WarpNodeType::kHardwareSource;
|
||||||
}
|
}
|
||||||
if (mc == "Stream/Output/Audio" || mc == "Stream/Input/Audio") {
|
if (mc == "Stream/Output/Audio" || mc == "Stream/Input/Audio") {
|
||||||
return WarpNodeType::kApplication;
|
return WarpNodeType::kApplication;
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
warppipe::ConnectionOptions opts;
|
warppipe::ConnectionOptions opts;
|
||||||
opts.application_name = "warppipe-gui";
|
opts.application_name = "warppipe-gui";
|
||||||
|
opts.config_path =
|
||||||
|
(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
|
||||||
|
QStringLiteral("/config.json"))
|
||||||
|
.toStdString();
|
||||||
|
|
||||||
auto result = warppipe::Client::Create(opts);
|
auto result = warppipe::Client::Create(opts);
|
||||||
if (!result.ok()) {
|
if (!result.ok()) {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ struct NodeInfo {
|
||||||
std::string application_name;
|
std::string application_name;
|
||||||
std::string process_binary;
|
std::string process_binary;
|
||||||
std::string media_role;
|
std::string media_role;
|
||||||
|
bool is_virtual = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PortInfo {
|
struct PortInfo {
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,8 @@ void Client::Impl::RegistryGlobal(void* data,
|
||||||
info.application_name = LookupString(props, PW_KEY_APP_NAME);
|
info.application_name = LookupString(props, PW_KEY_APP_NAME);
|
||||||
info.process_binary = LookupString(props, PW_KEY_APP_PROCESS_BINARY);
|
info.process_binary = LookupString(props, PW_KEY_APP_PROCESS_BINARY);
|
||||||
info.media_role = LookupString(props, PW_KEY_MEDIA_ROLE);
|
info.media_role = LookupString(props, PW_KEY_MEDIA_ROLE);
|
||||||
|
std::string virt_str = LookupString(props, PW_KEY_NODE_VIRTUAL);
|
||||||
|
info.is_virtual = (virt_str == "true");
|
||||||
impl->nodes[id] = info;
|
impl->nodes[id] = info;
|
||||||
impl->CheckRulesForNode(info);
|
impl->CheckRulesForNode(info);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1098,7 +1100,13 @@ Result<std::vector<NodeInfo>> Client::ListNodes() {
|
||||||
std::vector<NodeInfo> items;
|
std::vector<NodeInfo> items;
|
||||||
items.reserve(impl_->nodes.size());
|
items.reserve(impl_->nodes.size());
|
||||||
for (const auto& entry : impl_->nodes) {
|
for (const auto& entry : impl_->nodes) {
|
||||||
items.push_back(entry.second);
|
NodeInfo info = entry.second;
|
||||||
|
if (!info.is_virtual &&
|
||||||
|
impl_->virtual_streams.find(entry.first) !=
|
||||||
|
impl_->virtual_streams.end()) {
|
||||||
|
info.is_virtual = true;
|
||||||
|
}
|
||||||
|
items.push_back(std::move(info));
|
||||||
}
|
}
|
||||||
return {Status::Ok(), std::move(items)};
|
return {Status::Ok(), std::move(items)};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,15 @@ struct TestClient {
|
||||||
warppipe::NodeInfo MakeNode(uint32_t id, const std::string &name,
|
warppipe::NodeInfo MakeNode(uint32_t id, const std::string &name,
|
||||||
const std::string &media_class,
|
const std::string &media_class,
|
||||||
const std::string &app_name = {},
|
const std::string &app_name = {},
|
||||||
const std::string &desc = {}) {
|
const std::string &desc = {},
|
||||||
|
bool is_virtual = false) {
|
||||||
warppipe::NodeInfo n;
|
warppipe::NodeInfo n;
|
||||||
n.id = warppipe::NodeId{id};
|
n.id = warppipe::NodeId{id};
|
||||||
n.name = name;
|
n.name = name;
|
||||||
n.media_class = media_class;
|
n.media_class = media_class;
|
||||||
n.application_name = app_name;
|
n.application_name = app_name;
|
||||||
n.description = desc;
|
n.description = desc;
|
||||||
|
n.is_virtual = is_virtual;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,12 +88,12 @@ TEST_CASE("classifyNode identifies hardware source") {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("classifyNode identifies virtual sink") {
|
TEST_CASE("classifyNode identifies virtual sink") {
|
||||||
auto n = MakeNode(3, "warppipe-gaming-sink", "Audio/Sink");
|
auto n = MakeNode(3, "gaming-sink", "Audio/Sink", {}, {}, true);
|
||||||
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSink);
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSink);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("classifyNode identifies virtual source") {
|
TEST_CASE("classifyNode identifies virtual source") {
|
||||||
auto n = MakeNode(4, "warppipe-mic", "Audio/Source");
|
auto n = MakeNode(4, "my-mic", "Audio/Source", {}, {}, true);
|
||||||
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSource);
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,6 +112,26 @@ TEST_CASE("classifyNode returns unknown for unrecognized media class") {
|
||||||
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kUnknown);
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kUnknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("classifyNode virtual sink without warppipe in name") {
|
||||||
|
auto n = MakeNode(10, "Sink", "Audio/Sink", {}, {}, true);
|
||||||
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("classifyNode virtual source without warppipe in name") {
|
||||||
|
auto n = MakeNode(11, "Mic", "Audio/Source", {}, {}, true);
|
||||||
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("classifyNode non-virtual sink with warppipe in name") {
|
||||||
|
auto n = MakeNode(12, "warppipe-hw", "Audio/Sink", {}, {}, false);
|
||||||
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kHardwareSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("classifyNode virtual duplex treated as virtual sink") {
|
||||||
|
auto n = MakeNode(13, "my-duplex", "Audio/Duplex", {}, {}, true);
|
||||||
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kVirtualSink);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("classifyNode duplex treated as sink") {
|
TEST_CASE("classifyNode duplex treated as sink") {
|
||||||
auto n = MakeNode(8, "alsa_duplex", "Audio/Duplex");
|
auto n = MakeNode(8, "alsa_duplex", "Audio/Duplex");
|
||||||
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kHardwareSink);
|
REQUIRE(WarpGraphModel::classifyNode(n) == WarpNodeType::kHardwareSink);
|
||||||
|
|
@ -217,7 +239,7 @@ TEST_CASE("node style varies by type") {
|
||||||
REQUIRE(tc.client->Test_InsertNode(
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
MakeNode(100040, "hw-sink", "Audio/Sink")).ok());
|
MakeNode(100040, "hw-sink", "Audio/Sink")).ok());
|
||||||
REQUIRE(tc.client->Test_InsertNode(
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
MakeNode(100041, "warppipe-vsink", "Audio/Sink")).ok());
|
MakeNode(100041, "my-vsink", "Audio/Sink", {}, {}, true)).ok());
|
||||||
REQUIRE(tc.client->Test_InsertNode(
|
REQUIRE(tc.client->Test_InsertNode(
|
||||||
MakeNode(100042, "app-stream", "Stream/Output/Audio", "App")).ok());
|
MakeNode(100042, "app-stream", "Stream/Output/Audio", "App")).ok());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue