diff --git a/src/ui/ControlsPanel.cpp b/src/ui/ControlsPanel.cpp index 2ff19a2..1da7231 100644 --- a/src/ui/ControlsPanel.cpp +++ b/src/ui/ControlsPanel.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -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); } diff --git a/src/ui/ControlsPanel.h b/src/ui/ControlsPanel.h index 7fad0a5..5195c76 100644 --- a/src/ui/ControlsPanel.h +++ b/src/ui/ControlsPanel.h @@ -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;