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

View file

@ -549,6 +549,9 @@ void PipeWireGraphModel::autoArrange()
void PipeWireGraphModel::loadLayout() void PipeWireGraphModel::loadLayout()
{ {
m_layoutByStableId.clear(); m_layoutByStableId.clear();
m_hasViewState = false;
m_hasSplitterSizes = false;
m_splitterSizes.clear();
const QString path = layoutFilePath(); const QString path = layoutFilePath();
if (path.isEmpty()) { if (path.isEmpty()) {
return; return;
@ -578,6 +581,19 @@ void PipeWireGraphModel::loadLayout()
m_viewCenter = QPointF(x, y); m_viewCenter = QPointF(x, y);
m_hasViewState = true; 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_x"] = m_viewCenter.x();
view["center_y"] = m_viewCenter.y(); view["center_y"] = m_viewCenter.y();
root["view"] = view; 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); QFile file(path);
QDir().mkpath(QFileInfo(path).absolutePath()); QDir().mkpath(QFileInfo(path).absolutePath());
@ -826,4 +849,20 @@ bool PipeWireGraphModel::viewState(double &scale, QPointF &center) const
center = m_viewCenter; center = m_viewCenter;
return true; 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> #include <QtNodes/StyleCollection>

View file

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