scroll the controls panel's rows instead of clipping them

This commit is contained in:
2026-08-07 20:42:56 +02:00
parent 40415e663b
commit 0b8f5eccd6
2 changed files with 59 additions and 16 deletions

View File

@@ -5,6 +5,8 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QVBoxLayout>
@@ -15,10 +17,15 @@
namespace
{
const int kMarginPx = 8; // between the band's edge and the panel
const int kMarginPx = 8; // between the view's edge and the panel
const int kCardMarginPx = 8; // inside the panel, around its content
const int kHeadingGapPx = 4; // between the heading and the rows
const int kRefreshMs = 50; // see the class comment on why this polls
// The panel's border, from the stylesheet below. Spelled out because the stylesheet box
// is what sets it and asking the style for it before the first show is unreliable.
const int kBorderPx = 1;
// Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT
@@ -117,7 +124,7 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
QVBoxLayout* outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
kCardMarginPx, kCardMarginPx);
outerLayout->setSpacing(4);
outerLayout->setSpacing(kHeadingGapPx);
m_heading = new QLabel(this);
m_heading->setObjectName(QStringLiteral("controlHeading"));
@@ -125,11 +132,24 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
outerLayout->addWidget(m_heading);
// Rows taller than the space available scroll rather than being cut off
// (REQ-UI-CONTROLS-PANEL). The viewport is transparent so the panel's own rounded
// chrome shows through, and horizontal scrolling is off because the width always
// follows the content.
m_rows = new QWidget(this);
m_rowsLayout = new QVBoxLayout(m_rows);
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
m_rowsLayout->setSpacing(0);
outerLayout->addWidget(m_rows);
m_rows->setAutoFillBackground(false);
m_scrollArea = new QScrollArea(this);
m_scrollArea->setFrameShape(QFrame::NoFrame);
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scrollArea->viewport()->setAutoFillBackground(false);
m_scrollArea->setWidget(m_rows);
outerLayout->addWidget(m_scrollArea);
// Polling rather than subscribing; see the class comment.
m_refreshTimer = new QTimer(this);
@@ -153,7 +173,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
if (m_heading->geometry().contains(event->pos()))
{
m_collapsed = !m_collapsed;
m_rows->setVisible(!m_collapsed);
m_scrollArea->setVisible(!m_collapsed);
refit();
}
event->accept();
@@ -220,7 +240,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
addAndShow(m_rowsLayout, makeRow(action, context, m_rows));
}
m_rows->setVisible(!m_collapsed);
m_scrollArea->setVisible(!m_collapsed);
refit();
}
@@ -270,22 +290,33 @@ void ControlsPanel::refit()
m_rowsLayout->invalidate();
m_rowsLayout->activate();
// Deliberately not activating the panel's own layout here. That lays the heading
// and the rows out inside the geometry the panel still has from the previous
// context, which is the wrong frame of reference for choosing the new one.
// totalSizeHint answers "how big does this need to be" without reference to how big
// it currently is; setGeometry below then re-runs the outer layout on its own.
layout()->invalidate();
const QSize needed = layout()->totalSizeHint();
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
if (band.width() <= 0 || band.height() <= 0) { return; }
// Measured from the heading and the rows directly rather than from the panel's own
// layout: the rows now sit in a scroll area, whose size hint describes a viewport
// and says nothing about how tall its contents are. Deliberately not activating the
// panel's own layout either -- that lays its children out inside the geometry left
// over from the previous context, which is the wrong frame of reference for
// choosing the new one. setGeometry below re-runs it.
const int chromePx = 2 * (kCardMarginPx + kBorderPx);
const QSize headingHint = m_heading->sizeHint();
const QSize rowsHint = m_rows->sizeHint();
int contentWidthPx = headingHint.width();
int contentHeightPx = headingHint.height();
if (!m_collapsed)
{
contentWidthPx = qMax(contentWidthPx, rowsHint.width());
contentHeightPx += kHeadingGapPx + rowsHint.height();
}
const int wantedHeightPx = contentHeightPx + chromePx;
// The bottom-left corner of the view, growing upward as rows are added
// (REQ-UI-CONTROLS-PANEL).
const int widthPx = qMin(needed.width(), band.width());
int heightPx = qMin(needed.height(), band.height());
int topPx = band.bottom() - heightPx + 1;
int widthPx = qMin(contentWidthPx + chromePx, band.width());
int heightPx = qMin(wantedHeightPx, band.height());
int topPx = band.bottom() - heightPx + 1;
// The build button bar is centered and sized to its buttons, so it usually leaves
// this corner free and the panel can share the bottom edge with it. Only when the
@@ -299,5 +330,13 @@ void ControlsPanel::refit()
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
}
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
// own width, or it would appear over the labels.
if (heightPx < wantedHeightPx)
{
widthPx = qMin(widthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
band.width());
}
setGeometry(band.left(), topPx, widthPx, heightPx);
}

View File

@@ -10,6 +10,7 @@
class GameWorldView;
class QLabel;
class QScrollArea;
class QTimer;
class QVBoxLayout;
@@ -64,6 +65,9 @@ private:
const GameWorldView* m_view;
QLabel* m_heading;
// Scrolls the rows once they outgrow the space the panel has (REQ-UI-CONTROLS-PANEL).
// The heading is deliberately outside it, so it stays put and stays clickable.
QScrollArea* m_scrollArea;
QWidget* m_rows;
QVBoxLayout* m_rowsLayout;
QTimer* m_refreshTimer;