Compare commits

...

4 commits

Author SHA1 Message Date
5bb41373b2 Fix black lines 2026-02-06 11:19:50 -07:00
5fa5a63d1a Fix perf issues zoomed in again 2026-02-06 11:17:15 -07:00
69d9a9e3f1 More tests 2026-02-06 11:07:01 -07:00
16fc02837a Tests 2026-02-06 10:21:42 -07:00
6 changed files with 1602 additions and 28 deletions

View file

@ -39,6 +39,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QMimeData> #include <QMimeData>
#include <QPixmap> #include <QPixmap>
#include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea> #include <QScrollArea>
#include <QSplitter> #include <QSplitter>
@ -614,9 +615,19 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
&GraphEditorWidget::onRefreshTimer); &GraphEditorWidget::onRefreshTimer);
if (m_client) { if (m_client) {
m_client->SetChangeCallback([this] { QPointer<QTimer> changeTimer = m_changeTimer;
QMetaObject::invokeMethod(m_changeTimer, m_client->SetChangeCallback([changeTimer] {
qOverload<>(&QTimer::start), auto *app = QCoreApplication::instance();
if (!app) {
return;
}
QMetaObject::invokeMethod(app,
[changeTimer]() {
if (changeTimer) {
changeTimer->start();
}
},
Qt::QueuedConnection); Qt::QueuedConnection);
}); });
} }
@ -636,6 +647,11 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
void GraphEditorWidget::onRefreshTimer() { void GraphEditorWidget::onRefreshTimer() {
m_model->refreshFromClient(); m_model->refreshFromClient();
if (m_scene &&
m_scene->itemIndexMethod() != QGraphicsScene::BspTreeIndex) {
m_scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
}
if (!m_graphReady && m_model->allNodeIds().size() > 0) { if (!m_graphReady && m_model->allNodeIds().size() > 0) {
m_graphReady = true; m_graphReady = true;
Q_EMIT graphReady(); Q_EMIT graphReady();
@ -648,6 +664,13 @@ void GraphEditorWidget::scheduleSaveLayout() {
} }
GraphEditorWidget::~GraphEditorWidget() { GraphEditorWidget::~GraphEditorWidget() {
if (m_scene) {
disconnect(m_scene, nullptr, this, nullptr);
}
if (m_model) {
disconnect(m_model, nullptr, this, nullptr);
}
if (m_client) { if (m_client) {
m_client->SetChangeCallback(nullptr); m_client->SetChangeCallback(nullptr);
} }
@ -670,6 +693,45 @@ int GraphEditorWidget::linkCount() const {
return count / 2; return count / 2;
} }
QAction *GraphEditorWidget::execMenuAction(QMenu &menu,
const QPoint &screenPos) {
return menu.exec(screenPos);
}
QString GraphEditorWidget::promptTextInput(const QString &title,
const QString &label,
bool *ok) {
return QInputDialog::getText(this,
title,
label,
QLineEdit::Normal,
QString(),
ok);
}
QString GraphEditorWidget::chooseSaveFilePath(const QString &title,
const QString &initialDir,
const QString &filter) {
return QFileDialog::getSaveFileName(this,
title,
initialDir,
filter);
}
QString GraphEditorWidget::chooseOpenFilePath(const QString &title,
const QString &initialDir,
const QString &filter) {
return QFileDialog::getOpenFileName(this,
title,
initialDir,
filter);
}
void GraphEditorWidget::showWarningDialog(const QString &title,
const QString &message) {
QMessageBox::warning(this, title, message);
}
void GraphEditorWidget::setDebugScreenshotDir(const QString &dir) { void GraphEditorWidget::setDebugScreenshotDir(const QString &dir) {
m_debugScreenshotDir = dir; m_debugScreenshotDir = dir;
QDir d(dir); QDir d(dir);
@ -812,7 +874,7 @@ void GraphEditorWidget::showCanvasContextMenu(const QPoint &screenPos,
QAction *savePresetAction = menu.addAction(QStringLiteral("Save Preset...")); QAction *savePresetAction = menu.addAction(QStringLiteral("Save Preset..."));
QAction *loadPresetAction = menu.addAction(QStringLiteral("Load Preset...")); QAction *loadPresetAction = menu.addAction(QStringLiteral("Load Preset..."));
QAction *chosen = menu.exec(screenPos); QAction *chosen = execMenuAction(menu, screenPos);
if (chosen == createSink) { if (chosen == createSink) {
createVirtualNode(true, scenePos); createVirtualNode(true, scenePos);
} else if (chosen == createSource) { } else if (chosen == createSource) {
@ -904,7 +966,7 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
QStringLiteral( QStringLiteral(
"application/warppipe-virtual-graph")))); "application/warppipe-virtual-graph"))));
QAction *chosen = menu.exec(screenPos); QAction *chosen = execMenuAction(menu, screenPos);
if (!chosen) { if (!chosen) {
return; return;
} }
@ -931,9 +993,9 @@ void GraphEditorWidget::createVirtualNode(bool isSink,
const QPointF &scenePos) { const QPointF &scenePos) {
if (isSink) { if (isSink) {
bool ok = false; bool ok = false;
QString name = QInputDialog::getText( QString name = promptTextInput(QStringLiteral("Create Virtual Sink"),
this, QStringLiteral("Create Virtual Sink"), QStringLiteral("Node name:"),
QStringLiteral("Node name:"), QLineEdit::Normal, QString(), &ok); &ok);
if (!ok || name.trimmed().isEmpty()) if (!ok || name.trimmed().isEmpty())
return; return;
@ -941,8 +1003,8 @@ void GraphEditorWidget::createVirtualNode(bool isSink,
m_model->setPendingPosition(nodeName, scenePos); m_model->setPendingPosition(nodeName, scenePos);
auto result = m_client->CreateVirtualSink(nodeName); auto result = m_client->CreateVirtualSink(nodeName);
if (!result.status.ok()) { if (!result.status.ok()) {
QMessageBox::warning(this, QStringLiteral("Error"), showWarningDialog(QStringLiteral("Error"),
QString::fromStdString(result.status.message)); QString::fromStdString(result.status.message));
return; return;
} }
m_model->refreshFromClient(); m_model->refreshFromClient();
@ -1016,8 +1078,8 @@ void GraphEditorWidget::createVirtualNode(bool isSink,
QString name = nameEdit->text().trimmed(); QString name = nameEdit->text().trimmed();
if (name.isEmpty()) { if (name.isEmpty()) {
QMessageBox::warning(this, QStringLiteral("Error"), showWarningDialog(QStringLiteral("Error"),
QStringLiteral("Name cannot be empty.")); QStringLiteral("Name cannot be empty."));
return; return;
} }
@ -1032,8 +1094,8 @@ void GraphEditorWidget::createVirtualNode(bool isSink,
auto result = m_client->CreateVirtualSource(nodeName, opts); auto result = m_client->CreateVirtualSource(nodeName, opts);
if (!result.status.ok()) { if (!result.status.ok()) {
QMessageBox::warning(this, QStringLiteral("Error"), showWarningDialog(QStringLiteral("Error"),
QString::fromStdString(result.status.message)); QString::fromStdString(result.status.message));
return; return;
} }
m_model->refreshFromClient(); m_model->refreshFromClient();
@ -1397,9 +1459,9 @@ void GraphEditorWidget::savePreset() {
if (!dir.exists()) if (!dir.exists())
dir.mkpath("."); dir.mkpath(".");
QString path = QFileDialog::getSaveFileName( QString path = chooseSaveFilePath(QStringLiteral("Save Preset"),
this, QStringLiteral("Save Preset"), m_presetDir, m_presetDir,
QStringLiteral("JSON files (*.json)")); QStringLiteral("JSON files (*.json)"));
if (path.isEmpty()) if (path.isEmpty())
return; return;
if (!path.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive)) if (!path.endsWith(QStringLiteral(".json"), Qt::CaseInsensitive))
@ -1410,15 +1472,15 @@ void GraphEditorWidget::savePreset() {
mw->statusBar()->showMessage( mw->statusBar()->showMessage(
QStringLiteral("Preset saved: ") + QFileInfo(path).fileName(), 4000); QStringLiteral("Preset saved: ") + QFileInfo(path).fileName(), 4000);
} else { } else {
QMessageBox::warning(this, QStringLiteral("Error"), showWarningDialog(QStringLiteral("Error"),
QStringLiteral("Failed to save preset.")); QStringLiteral("Failed to save preset."));
} }
} }
void GraphEditorWidget::loadPreset() { void GraphEditorWidget::loadPreset() {
QString path = QFileDialog::getOpenFileName( QString path = chooseOpenFilePath(QStringLiteral("Load Preset"),
this, QStringLiteral("Load Preset"), m_presetDir, m_presetDir,
QStringLiteral("JSON files (*.json)")); QStringLiteral("JSON files (*.json)"));
if (path.isEmpty()) if (path.isEmpty())
return; return;
@ -1427,8 +1489,8 @@ void GraphEditorWidget::loadPreset() {
mw->statusBar()->showMessage( mw->statusBar()->showMessage(
QStringLiteral("Preset loaded: ") + QFileInfo(path).fileName(), 4000); QStringLiteral("Preset loaded: ") + QFileInfo(path).fileName(), 4000);
} else { } else {
QMessageBox::warning(this, QStringLiteral("Error"), showWarningDialog(QStringLiteral("Error"),
QStringLiteral("Failed to load preset.")); QStringLiteral("Failed to load preset."));
} }
} }

View file

@ -27,6 +27,8 @@ class QSplitter;
class QTabWidget; class QTabWidget;
class QSlider; class QSlider;
class QTimer; class QTimer;
class QAction;
class QMenu;
class DeleteVirtualNodeCommand; class DeleteVirtualNodeCommand;
enum class ConnectionStyleType : uint8_t { enum class ConnectionStyleType : uint8_t {
@ -59,6 +61,20 @@ private slots:
void onContextMenuRequested(const QPoint &pos); void onContextMenuRequested(const QPoint &pos);
void scheduleSaveLayout(); void scheduleSaveLayout();
protected:
virtual QAction *execMenuAction(QMenu &menu, const QPoint &screenPos);
virtual QString promptTextInput(const QString &title,
const QString &label,
bool *ok);
virtual QString chooseSaveFilePath(const QString &title,
const QString &initialDir,
const QString &filter);
virtual QString chooseOpenFilePath(const QString &title,
const QString &initialDir,
const QString &filter);
virtual void showWarningDialog(const QString &title,
const QString &message);
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,

View file

@ -352,12 +352,11 @@ void WarpGraphModel::refreshFromClient() {
return; return;
} }
Q_EMIT beginBatchUpdate();
m_refreshing = true; m_refreshing = true;
bool sceneChanged = false;
auto nodesResult = m_client->ListNodes(); auto nodesResult = m_client->ListNodes();
if (!nodesResult.ok()) { if (!nodesResult.ok()) {
m_refreshing = false; m_refreshing = false;
Q_EMIT endBatchUpdate();
return; return;
} }
@ -512,6 +511,10 @@ void WarpGraphModel::refreshFromClient() {
m_volumeStates[qtId] = {}; m_volumeStates[qtId] = {};
} }
if (!sceneChanged) {
sceneChanged = true;
Q_EMIT beginBatchUpdate();
}
Q_EMIT nodeCreated(qtId); Q_EMIT nodeCreated(qtId);
} }
@ -526,6 +529,10 @@ void WarpGraphModel::refreshFromClient() {
if (it == m_pwToQt.end()) { if (it == m_pwToQt.end()) {
continue; continue;
} }
if (!sceneChanged) {
sceneChanged = true;
Q_EMIT beginBatchUpdate();
}
QtNodes::NodeId qtId = it->second; QtNodes::NodeId qtId = it->second;
auto nodeIt = m_nodes.find(qtId); auto nodeIt = m_nodes.find(qtId);
if (nodeIt == m_nodes.end()) { if (nodeIt == m_nodes.end()) {
@ -581,6 +588,10 @@ void WarpGraphModel::refreshFromClient() {
QtNodes::ConnectionId connId{outNodeIt->second, outPortIdx, QtNodes::ConnectionId connId{outNodeIt->second, outPortIdx,
inNodeIt->second, inPortIdx}; inNodeIt->second, inPortIdx};
if (m_connections.find(connId) == m_connections.end()) { if (m_connections.find(connId) == m_connections.end()) {
if (!sceneChanged) {
sceneChanged = true;
Q_EMIT beginBatchUpdate();
}
m_connections.insert(connId); m_connections.insert(connId);
m_linkIdToConn.emplace(link.id.value, connId); m_linkIdToConn.emplace(link.id.value, connId);
Q_EMIT connectionCreated(connId); Q_EMIT connectionCreated(connId);
@ -609,6 +620,10 @@ void WarpGraphModel::refreshFromClient() {
{ {
auto connIt = m_connections.find(connId); auto connIt = m_connections.find(connId);
if (connIt != m_connections.end()) { if (connIt != m_connections.end()) {
if (!sceneChanged) {
sceneChanged = true;
Q_EMIT beginBatchUpdate();
}
m_connections.erase(connIt); m_connections.erase(connIt);
Q_EMIT connectionDeleted(connId); Q_EMIT connectionDeleted(connId);
} }
@ -696,7 +711,9 @@ void WarpGraphModel::refreshFromClient() {
} }
m_refreshing = false; m_refreshing = false;
Q_EMIT endBatchUpdate(); if (sceneChanged) {
Q_EMIT endBatchUpdate();
}
} }
const WarpNodeData * const WarpNodeData *

View file

@ -18,7 +18,7 @@ public:
explicit ZoomGraphicsView(QtNodes::BasicGraphicsScene *scene, explicit ZoomGraphicsView(QtNodes::BasicGraphicsScene *scene,
QWidget *parent = nullptr) QWidget *parent = nullptr)
: QtNodes::GraphicsView(scene, parent) { : QtNodes::GraphicsView(scene, parent) {
setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setCacheMode(QGraphicsView::CacheNone); setCacheMode(QGraphicsView::CacheNone);
} }

File diff suppressed because it is too large Load diff

View file

@ -626,6 +626,60 @@ TEST_CASE("set default sink without metadata returns unavailable") {
REQUIRE_FALSE(status.ok()); REQUIRE_FALSE(status.ok());
} }
TEST_CASE("set default source without metadata returns unavailable") {
auto result = warppipe::Client::Create(DefaultOptions());
if (!result.ok()) {
SUCCEED("PipeWire unavailable");
return;
}
auto status = result.value->SetDefaultSource("");
REQUIRE_FALSE(status.ok());
}
TEST_CASE("GetVirtualNodeInfo returns details for created virtual sink") {
auto result = warppipe::Client::Create(DefaultOptions());
if (!result.ok()) {
SUCCEED("PipeWire unavailable");
return;
}
warppipe::VirtualNodeOptions options;
options.display_name = "warppipe-test-info";
options.group = "warppipe-test";
options.format.rate = 48000;
options.format.channels = 2;
auto sink = result.value->CreateVirtualSink("warppipe-info-sink", options);
if (!sink.ok()) {
if (sink.status.code == warppipe::StatusCode::kUnavailable) {
SUCCEED("PipeWire unavailable");
return;
}
REQUIRE(sink.ok());
}
auto info = result.value->GetVirtualNodeInfo(sink.value.node);
REQUIRE(info.ok());
REQUIRE(info.value.node.value == sink.value.node.value);
REQUIRE(info.value.name == sink.value.name);
REQUIRE_FALSE(info.value.is_source);
REQUIRE(result.value->RemoveNode(sink.value.node).ok());
}
TEST_CASE("GetVirtualNodeInfo missing node returns not found") {
auto result = warppipe::Client::Create(DefaultOptions());
if (!result.ok()) {
SUCCEED("PipeWire unavailable");
return;
}
auto info = result.value->GetVirtualNodeInfo(warppipe::NodeId{999999});
REQUIRE_FALSE(info.ok());
REQUIRE(info.status.code == warppipe::StatusCode::kNotFound);
}
TEST_CASE("NodeInfo captures application properties") { TEST_CASE("NodeInfo captures application properties") {
auto result = warppipe::Client::Create(DefaultOptions()); auto result = warppipe::Client::Create(DefaultOptions());
if (!result.ok()) { if (!result.ok()) {