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

View file

@ -27,6 +27,8 @@ class QSplitter;
class QTabWidget;
class QSlider;
class QTimer;
class QAction;
class QMenu;
class DeleteVirtualNodeCommand;
enum class ConnectionStyleType : uint8_t {
@ -59,6 +61,20 @@ private slots:
void onContextMenuRequested(const QPoint &pos);
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:
void showCanvasContextMenu(const QPoint &screenPos, const QPointF &scenePos);
void showNodeContextMenu(const QPoint &screenPos, uint32_t pwNodeId,

View file

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

View file

@ -18,7 +18,7 @@ public:
explicit ZoomGraphicsView(QtNodes::BasicGraphicsScene *scene,
QWidget *parent = nullptr)
: QtNodes::GraphicsView(scene, parent) {
setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
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());
}
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") {
auto result = warppipe::Client::Create(DefaultOptions());
if (!result.ok()) {