GUI M8f: Event-driven updates, deferred link restoration, routing rules UI

This commit is contained in:
Joey Yakimowich-Payne 2026-01-30 11:52:24 -07:00
commit b819d6fd65
6 changed files with 440 additions and 121 deletions

View file

@ -14,10 +14,14 @@
#include <QAction>
#include <QClipboard>
#include <QContextMenuEvent>
#include <QComboBox>
#include <QCoreApplication>
#include <QDateTime>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QFormLayout>
#include <QGraphicsItem>
#include <QGuiApplication>
#include <QInputDialog>
@ -285,6 +289,18 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
m_sidebar->addTab(m_mixerScroll, QStringLiteral("MIXER"));
m_sidebar->addTab(presetsTab, QStringLiteral("PRESETS"));
m_rulesScroll = new QScrollArea();
m_rulesScroll->setWidgetResizable(true);
m_rulesScroll->setStyleSheet(m_mixerScroll->styleSheet());
m_rulesContainer = new QWidget();
m_rulesContainer->setStyleSheet(QStringLiteral("background: #1a1a1e;"));
auto *rulesLayout = new QVBoxLayout(m_rulesContainer);
rulesLayout->setContentsMargins(8, 8, 8, 8);
rulesLayout->setSpacing(6);
rulesLayout->addStretch();
m_rulesScroll->setWidget(m_rulesContainer);
m_sidebar->addTab(m_rulesScroll, QStringLiteral("RULES"));
m_splitter = new QSplitter(Qt::Horizontal);
m_splitter->addWidget(m_view);
m_splitter->addWidget(m_sidebar);
@ -405,6 +421,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
wireVolumeWidget(nodeId);
rebuildMixerStrips();
rebuildNodeMeters();
rebuildRulesList();
});
connect(m_model, &QtNodes::AbstractGraphModel::nodeDeleted, this,
[this](QtNodes::NodeId nodeId) {
@ -412,6 +429,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
m_nodeMeters.erase(nodeId);
rebuildMixerStrips();
rebuildNodeMeters();
rebuildRulesList();
});
m_saveTimer = new QTimer(this);
@ -421,6 +439,7 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
&GraphEditorWidget::saveLayoutWithViewState);
m_model->refreshFromClient();
rebuildRulesList();
if (!hasLayout) {
m_model->autoArrange();
}
@ -435,10 +454,24 @@ GraphEditorWidget::GraphEditorWidget(warppipe::Client *client,
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
&GraphEditorWidget::saveLayoutWithViewState);
m_changeTimer = new QTimer(this);
m_changeTimer->setSingleShot(true);
m_changeTimer->setInterval(50);
connect(m_changeTimer, &QTimer::timeout, this,
&GraphEditorWidget::onRefreshTimer);
if (m_client) {
m_client->SetChangeCallback([this] {
QMetaObject::invokeMethod(m_changeTimer,
qOverload<>(&QTimer::start),
Qt::QueuedConnection);
});
}
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this,
&GraphEditorWidget::onRefreshTimer);
m_refreshTimer->start(500);
m_refreshTimer->start(2000);
m_meterTimer = new QTimer(this);
m_meterTimer->setTimerType(Qt::PreciseTimer);
@ -1373,3 +1406,186 @@ void GraphEditorWidget::rebuildNodeMeters() {
}
}
}
void GraphEditorWidget::rebuildRulesList() {
if (!m_rulesContainer || !m_client)
return;
auto *layout = m_rulesContainer->layout();
if (!layout)
return;
while (layout->count() > 0) {
auto *item = layout->takeAt(0);
if (item->widget())
item->widget()->deleteLater();
delete item;
}
const QString labelStyle = QStringLiteral(
"QLabel { color: #a0a8b6; font-size: 11px; background: transparent; }");
const QString valueStyle = QStringLiteral(
"QLabel { color: #ecf0f6; font-size: 12px; background: transparent; }");
const QString btnStyle = QStringLiteral(
"QPushButton { background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;"
" border-radius: 4px; padding: 6px 12px; }"
"QPushButton:hover { background: #3a3a44; }"
"QPushButton:pressed { background: #44444e; }");
const QString delBtnStyle = QStringLiteral(
"QPushButton { background: transparent; color: #a05050; border: none;"
" font-size: 14px; font-weight: bold; padding: 2px 6px; }"
"QPushButton:hover { color: #e05050; }");
auto *header = new QLabel(QStringLiteral("ROUTING RULES"));
header->setStyleSheet(QStringLiteral(
"QLabel { color: #a0a8b6; font-size: 11px; font-weight: bold;"
" background: transparent; }"));
layout->addWidget(header);
auto rulesResult = m_client->ListRouteRules();
if (rulesResult.ok()) {
for (const auto &rule : rulesResult.value) {
auto *card = new QWidget();
card->setStyleSheet(QStringLiteral(
"QWidget { background: #24242a; border-radius: 4px; }"));
auto *cardLayout = new QHBoxLayout(card);
cardLayout->setContentsMargins(8, 6, 4, 6);
cardLayout->setSpacing(8);
QString matchText;
if (!rule.match.application_name.empty())
matchText += QStringLiteral("app: ") +
QString::fromStdString(rule.match.application_name);
if (!rule.match.process_binary.empty()) {
if (!matchText.isEmpty()) matchText += QStringLiteral(", ");
matchText += QStringLiteral("bin: ") +
QString::fromStdString(rule.match.process_binary);
}
if (!rule.match.media_role.empty()) {
if (!matchText.isEmpty()) matchText += QStringLiteral(", ");
matchText += QStringLiteral("role: ") +
QString::fromStdString(rule.match.media_role);
}
auto *infoLayout = new QVBoxLayout();
infoLayout->setContentsMargins(0, 0, 0, 0);
infoLayout->setSpacing(2);
auto *matchLabel = new QLabel(matchText);
matchLabel->setStyleSheet(valueStyle);
infoLayout->addWidget(matchLabel);
auto *targetLabel = new QLabel(
QStringLiteral("\xe2\x86\x92 ") +
QString::fromStdString(rule.target_node));
targetLabel->setStyleSheet(labelStyle);
infoLayout->addWidget(targetLabel);
cardLayout->addLayout(infoLayout, 1);
auto *delBtn = new QPushButton(QStringLiteral("\xe2\x9c\x95"));
delBtn->setFixedSize(24, 24);
delBtn->setStyleSheet(delBtnStyle);
warppipe::RuleId ruleId = rule.id;
connect(delBtn, &QPushButton::clicked, this, [this, ruleId]() {
m_client->RemoveRouteRule(ruleId);
rebuildRulesList();
});
cardLayout->addWidget(delBtn);
layout->addWidget(card);
}
}
auto *addBtn = new QPushButton(QStringLiteral("Add Rule..."));
addBtn->setStyleSheet(btnStyle);
connect(addBtn, &QPushButton::clicked, this,
&GraphEditorWidget::showAddRuleDialog);
layout->addWidget(addBtn);
static_cast<QVBoxLayout *>(layout)->addStretch();
}
void GraphEditorWidget::showAddRuleDialog() {
if (!m_client)
return;
QDialog dlg(this);
dlg.setWindowTitle(QStringLiteral("Add Routing Rule"));
dlg.setStyleSheet(QStringLiteral(
"QDialog { background: #1e1e22; }"
"QLabel { color: #ecf0f6; }"
"QLineEdit { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;"
" border-radius: 4px; padding: 4px 8px; }"
"QComboBox { background: #2a2a32; color: #ecf0f6; border: 1px solid #3a3a44;"
" border-radius: 4px; padding: 4px 8px; }"
"QComboBox::drop-down { border: none; }"
"QComboBox QAbstractItemView { background: #2a2a32; color: #ecf0f6;"
" selection-background-color: #3a3a44; }"));
auto *form = new QFormLayout(&dlg);
form->setContentsMargins(16, 16, 16, 16);
form->setSpacing(8);
auto *appNameEdit = new QLineEdit();
appNameEdit->setPlaceholderText(QStringLiteral("e.g. Firefox"));
form->addRow(QStringLiteral("Application Name:"), appNameEdit);
auto *processBinEdit = new QLineEdit();
processBinEdit->setPlaceholderText(QStringLiteral("e.g. firefox"));
form->addRow(QStringLiteral("Process Binary:"), processBinEdit);
auto *mediaRoleEdit = new QLineEdit();
mediaRoleEdit->setPlaceholderText(QStringLiteral("e.g. Music"));
form->addRow(QStringLiteral("Media Role:"), mediaRoleEdit);
auto *targetCombo = new QComboBox();
auto nodesResult = m_client->ListNodes();
if (nodesResult.ok()) {
for (const auto &node : nodesResult.value) {
if (node.media_class.find("Sink") != std::string::npos) {
QString label = QString::fromStdString(
node.description.empty() ? node.name : node.description);
targetCombo->addItem(label, QString::fromStdString(node.name));
}
}
}
form->addRow(QStringLiteral("Target Node:"), targetCombo);
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttons->setStyleSheet(QStringLiteral(
"QPushButton { background: #2e2e36; color: #ecf0f6; border: 1px solid #3a3a44;"
" border-radius: 4px; padding: 6px 16px; }"
"QPushButton:hover { background: #3a3a44; }"));
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
std::string appName = appNameEdit->text().trimmed().toStdString();
std::string procBin = processBinEdit->text().trimmed().toStdString();
std::string role = mediaRoleEdit->text().trimmed().toStdString();
std::string target = targetCombo->currentData().toString().toStdString();
if (appName.empty() && procBin.empty() && role.empty()) {
QMessageBox::warning(this, QStringLiteral("Invalid Rule"),
QStringLiteral("At least one match field must be filled."));
return;
}
if (target.empty()) {
QMessageBox::warning(this, QStringLiteral("Invalid Rule"),
QStringLiteral("A target node must be selected."));
return;
}
warppipe::RouteRule rule;
rule.match.application_name = appName;
rule.match.process_binary = procBin;
rule.match.media_role = role;
rule.target_node = target;
m_client->AddRouteRule(rule);
rebuildRulesList();
}