Expandable sidebar

This commit is contained in:
Joey Yakimowich-Payne 2026-01-27 17:53:50 -07:00
commit e326eabbf8
4 changed files with 125 additions and 17 deletions

View file

@ -10,8 +10,10 @@
#include <QHBoxLayout>
#include <QSplitter>
#include <QTimer>
#include <QEvent>
#include <QVBoxLayout>
#include <QScrollArea>
#include <QSizePolicy>
#include <QtNodes/GraphicsViewStyle>
#include <QtNodes/NodeStyle>
@ -59,12 +61,14 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
m_view = new QtNodes::GraphicsView(m_scene);
m_scene->setBackgroundBrush(QColor(28, 30, 34));
auto *splitter = new QSplitter(this);
splitter->setOrientation(Qt::Horizontal);
splitter->addWidget(m_view);
m_splitter = new QSplitter(this);
m_splitter->setOrientation(Qt::Horizontal);
m_splitter->addWidget(m_view);
auto *meterPanel = new QWidget(splitter);
auto *meterPanel = new QWidget(m_splitter);
meterPanel->setStyleSheet("background-color: #1f2126; border-left: 1px solid #2b2f38;");
meterPanel->setMinimumWidth(260);
meterPanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
auto *meterLayout = new QVBoxLayout(meterPanel);
meterLayout->setContentsMargins(20, 24, 20, 20);
@ -98,14 +102,13 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
meterLayout->addStretch();
meterPanel->setFixedWidth(320);
splitter->addWidget(meterPanel);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 0);
m_splitter->addWidget(meterPanel);
m_splitter->setStretchFactor(0, 1);
m_splitter->setStretchFactor(1, 0);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(splitter);
layout->addWidget(m_splitter);
setLayout(layout);
connect(m_model, &PipeWireGraphModel::connectionCreated,
@ -142,6 +145,7 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
connect(autoArrangeAction, &QAction::triggered, [this]() {
m_model->autoArrange();
m_view->zoomFitAll();
updateLayoutState();
m_model->saveLayout();
});
m_view->addAction(autoArrangeAction);
@ -154,6 +158,7 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
defaultPath,
QString("Layout Files (*.json)"));
if (!filePath.isEmpty()) {
updateLayoutState();
m_model->saveLayoutAs(filePath);
}
});
@ -163,6 +168,7 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
connect(resetLayoutAction, &QAction::triggered, [this]() {
m_model->resetLayout();
m_view->zoomFitAll();
updateLayoutState();
});
m_view->addAction(resetLayoutAction);
@ -182,12 +188,18 @@ GraphEditorWidget::GraphEditorWidget(Potato::PipeWireController *controller, QWi
QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this]() {
if (m_model) {
const QPointF center = m_view->mapToScene(m_view->viewport()->rect().center());
m_model->setViewState(m_view->getScale(), center);
updateLayoutState();
m_model->saveLayout();
}
});
QList<int> splitterSizes;
if (m_model->splitterSizes(splitterSizes)) {
m_splitter->setSizes(splitterSizes);
} else {
m_splitter->setSizes({1200, 320});
}
m_meterTimer = new QTimer(this);
m_meterTimer->setInterval(33);
m_meterTimer->setTimerType(Qt::PreciseTimer);
@ -224,8 +236,7 @@ void GraphEditorWidget::syncGraph()
void GraphEditorWidget::refreshGraph()
{
const QPointF center = m_view->mapToScene(m_view->viewport()->rect().center());
m_model->setViewState(m_view->getScale(), center);
updateLayoutState();
m_ignoreCreate.clear();
m_ignoreDelete.clear();
m_connectionToLinkId.clear();
@ -234,6 +245,7 @@ void GraphEditorWidget::refreshGraph()
m_model->reset();
syncGraph();
m_view->zoomFitAll();
updateLayoutState();
m_model->saveLayout();
}
@ -266,6 +278,10 @@ void GraphEditorWidget::onNodeRemoved(uint32_t nodeId)
if (m_nodeMeterRows.contains(nodeId)) {
QWidget *row = m_nodeMeterRows.take(nodeId);
if (m_nodeMeterLabels.contains(nodeId)) {
QLabel *label = m_nodeMeterLabels.take(nodeId);
label->removeEventFilter(this);
}
m_nodeMeters.remove(nodeId);
row->deleteLater();
}
@ -390,6 +406,19 @@ void GraphEditorWidget::updateMeter()
}
}
void GraphEditorWidget::updateLayoutState()
{
if (!m_model || !m_view) {
return;
}
const QPointF center = m_view->mapToScene(m_view->viewport()->rect().center());
m_model->setViewState(m_view->getScale(), center);
if (m_splitter) {
m_model->setSplitterSizes(m_splitter->sizes());
}
}
void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo &node)
{
if (m_nodeMeterRows.contains(nodeId)) {
@ -419,10 +448,9 @@ void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo
auto *label = new QLabel(title, row);
label->setStyleSheet("color: #c7cfdd; font-size: 11px; border: none;");
label->setToolTip(title);
const int labelWidth = 250;
label->setFixedWidth(labelWidth);
QFontMetrics metrics(label->font());
label->setText(metrics.elidedText(title, Qt::ElideRight, labelWidth));
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
label->setProperty("fullTitle", title);
label->installEventFilter(this);
auto *meter = new AudioLevelMeter(row);
meter->setMinimumHeight(70);
@ -436,4 +464,33 @@ void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo
m_nodeMeters.insert(nodeId, meter);
m_nodeMeterRows.insert(nodeId, row);
m_nodeMeterLabels.insert(nodeId, label);
updateNodeMeterLabel(label);
}
void GraphEditorWidget::updateNodeMeterLabel(QLabel *label)
{
if (!label) {
return;
}
const QString title = label->property("fullTitle").toString();
if (title.isEmpty()) {
return;
}
const int width = label->width() > 0 ? label->width() : label->sizeHint().width();
QFontMetrics metrics(label->font());
label->setText(metrics.elidedText(title, Qt::ElideRight, width));
}
bool GraphEditorWidget::eventFilter(QObject *object, QEvent *event)
{
if (auto *label = qobject_cast<QLabel*>(object)) {
if (event->type() == QEvent::Resize || event->type() == QEvent::Show) {
updateNodeMeterLabel(label);
}
}
return QWidget::eventFilter(object, event);
}

View file

@ -12,9 +12,11 @@
#include <cstdint>
class AudioLevelMeter;
class QLabel;
class QTimer;
class QScrollArea;
class QVBoxLayout;
class QSplitter;
class GraphEditorWidget : public QWidget
{
@ -37,12 +39,16 @@ private slots:
private:
void syncGraph();
void refreshGraph();
void updateLayoutState();
void updateNodeMeterLabel(QLabel *label);
QString connectionKey(const QtNodes::ConnectionId &connectionId) const;
bool eventFilter(QObject *object, QEvent *event) override;
Potato::PipeWireController *m_controller = nullptr;
PipeWireGraphModel *m_model = nullptr;
QtNodes::BasicGraphicsScene *m_scene = nullptr;
QtNodes::GraphicsView *m_view = nullptr;
QSplitter *m_splitter = nullptr;
QSet<QString> m_ignoreCreate;
QSet<QString> m_ignoreDelete;
@ -55,4 +61,5 @@ private:
QVBoxLayout *m_meterListLayout = nullptr;
QMap<uint32_t, AudioLevelMeter*> m_nodeMeters;
QMap<uint32_t, QWidget*> m_nodeMeterRows;
QMap<uint32_t, QLabel*> m_nodeMeterLabels;
};

View file

@ -549,6 +549,9 @@ void PipeWireGraphModel::autoArrange()
void PipeWireGraphModel::loadLayout()
{
m_layoutByStableId.clear();
m_hasViewState = false;
m_hasSplitterSizes = false;
m_splitterSizes.clear();
const QString path = layoutFilePath();
if (path.isEmpty()) {
return;
@ -578,6 +581,19 @@ void PipeWireGraphModel::loadLayout()
m_viewCenter = QPointF(x, y);
m_hasViewState = true;
}
const QJsonArray splitter = root.value("splitter").toArray();
if (!splitter.isEmpty()) {
QList<int> sizes;
sizes.reserve(splitter.size());
for (const auto &value : splitter) {
sizes.append(value.toInt());
}
if (!sizes.isEmpty()) {
m_splitterSizes = sizes;
m_hasSplitterSizes = true;
}
}
}
}
@ -786,6 +802,13 @@ void PipeWireGraphModel::writeLayoutToFile(const QString &path) const
view["center_x"] = m_viewCenter.x();
view["center_y"] = m_viewCenter.y();
root["view"] = view;
if (m_hasSplitterSizes && !m_splitterSizes.isEmpty()) {
QJsonArray splitter;
for (const auto size : m_splitterSizes) {
splitter.append(size);
}
root["splitter"] = splitter;
}
QFile file(path);
QDir().mkpath(QFileInfo(path).absolutePath());
@ -826,4 +849,20 @@ bool PipeWireGraphModel::viewState(double &scale, QPointF &center) const
center = m_viewCenter;
return true;
}
void PipeWireGraphModel::setSplitterSizes(const QList<int> &sizes)
{
m_splitterSizes = sizes;
m_hasSplitterSizes = !sizes.isEmpty();
}
bool PipeWireGraphModel::splitterSizes(QList<int> &sizes) const
{
if (!m_hasSplitterSizes) {
return false;
}
sizes = m_splitterSizes;
return !sizes.isEmpty();
}
#include <QtNodes/StyleCollection>

View file

@ -10,6 +10,7 @@
#include <QString>
#include <QJsonObject>
#include <QJsonArray>
#include <QList>
#include <cstdint>
#include <unordered_map>
@ -67,6 +68,8 @@ public:
QString defaultLayoutPath() const;
void setViewState(double scale, const QPointF &center);
bool viewState(double &scale, QPointF &center) const;
void setSplitterSizes(const QList<int> &sizes);
bool splitterSizes(QList<int> &sizes) const;
private:
QtNodes::ConnectionId connectionFromPipeWire(const Potato::LinkInfo &link, bool *ok) const;
@ -90,4 +93,6 @@ private:
QPointF m_viewCenter = QPointF(0, 0);
double m_viewScale = 1.0;
bool m_hasViewState = false;
QList<int> m_splitterSizes;
bool m_hasSplitterSizes = false;
};