From 365641feb70023190940c056b36e1f0041ab4e7d Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Fri, 7 Aug 2026 19:38:10 +0200 Subject: [PATCH] size the controls panel to the rows it is actually showing --- src/ui/ControlsPanel.cpp | 70 ++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/ui/ControlsPanel.cpp b/src/ui/ControlsPanel.cpp index 219433b..9cea8a2 100644 --- a/src/ui/ControlsPanel.cpp +++ b/src/ui/ControlsPanel.cpp @@ -1,5 +1,6 @@ #include "ControlsPanel.h" +#include #include #include #include @@ -21,6 +22,20 @@ const int kRefreshMs = 50; // see the class comment on why this polls // Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler". const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT +// The upper-case heading and caption are tracked out a little so they read as labels +// rather than as words. Set on the font because Qt's stylesheets have no letter-spacing +// property -- writing one there is silently ignored apart from a warning per widget. +QFont makeSpacedFont(QFont font, bool bold, int pointSizeDelta) +{ + font.setBold(bold); + if (pointSizeDelta != 0 && font.pointSize() > 0) + { + font.setPointSize(qMax(1, font.pointSize() + pointSizeDelta)); + } + font.setLetterSpacing(QFont::AbsoluteSpacing, 1.0); + return font; +} + // One row: the chips for the bindings, then what the action is called. Built as a plain // widget rather than a class of its own -- it holds no state and answers no questions. QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* parent) @@ -50,6 +65,18 @@ QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* p return row; } +// Adds a freshly built widget to the rows and shows it. +// +// The show is what makes it count. A widget created under an already-visible parent +// starts hidden, and a layout treats a hidden item as empty -- it contributes nothing +// to the size hint until something shows it, which otherwise does not happen until the +// event loop next runs, long after the panel has measured itself. +void addAndShow(QVBoxLayout* layout, QWidget* widget) +{ + layout->addWidget(widget); + widget->show(); +} + } // namespace ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent) @@ -63,19 +90,20 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent) // class scoped selector keeps the border on the panel rather than cascading onto // its children. setAttribute(Qt::WA_StyledBackground, true); + // Letter spacing is deliberately absent here: Qt's stylesheet syntax has no such + // property and warns on every widget it is applied to. The heading and the caption + // set it on their QFont instead. setStyleSheet(QStringLiteral( "ControlsPanel { background-color: palette(window);" " border: 1px solid palette(mid); border-radius: 4px; }" - "QLabel#controlHeading { font-weight: bold; letter-spacing: 1px;" - " color: palette(text); }" + "QLabel#controlHeading { color: palette(text); }" "QLabel#controlBadge { border: 1px solid palette(mid); border-radius: 3px;" " padding: 1px 5px; font-family: monospace; color: palette(text); }" "QLabel#controlLabel { color: palette(text); }" // The row that leaves the mode reads differently from the ones that act within // it (REQ-UI-CONTROLS-CARD). "QLabel#controlLabelExit { color: palette(bright-text); }" - "QLabel#controlCaption { color: palette(mid); font-size: 10px;" - " letter-spacing: 1px; }")); + "QLabel#controlCaption { color: palette(mid); }")); QVBoxLayout* outerLayout = new QVBoxLayout(this); outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx, @@ -85,6 +113,7 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent) m_heading = new QLabel(this); m_heading->setObjectName(QStringLiteral("controlHeading")); m_heading->setCursor(Qt::PointingHandCursor); + m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0)); outerLayout->addWidget(m_heading); m_rows = new QWidget(this); @@ -156,7 +185,7 @@ void ControlsPanel::rebuild(const ControlContext& context) for (ControlAction action : m_shownContextActions) { - m_rowsLayout->addWidget(makeRow(action, context, m_rows)); + addAndShow(m_rowsLayout, makeRow(action, context, m_rows)); } // The always-available block sits under a divider in every context, the General one @@ -168,16 +197,17 @@ void ControlsPanel::rebuild(const ControlContext& context) divider->setFrameShape(QFrame::HLine); divider->setFrameShadow(QFrame::Plain); m_rowsLayout->addSpacing(6); - m_rowsLayout->addWidget(divider); + addAndShow(m_rowsLayout, divider); QLabel* caption = new QLabel(tr("ALWAYS AVAILABLE"), m_rows); caption->setObjectName(QStringLiteral("controlCaption")); - m_rowsLayout->addWidget(caption); + caption->setFont(makeSpacedFont(font(), /*bold*/ false, /*pointSizeDelta*/ -1)); + addAndShow(m_rowsLayout, caption); } for (ControlAction action : m_shownAlwaysActions) { - m_rowsLayout->addWidget(makeRow(action, context, m_rows)); + addAndShow(m_rowsLayout, makeRow(action, context, m_rows)); } m_rows->setVisible(!m_collapsed); @@ -221,16 +251,28 @@ void ControlsPanel::refit() { if (m_bandRect.isNull()) { return; } - // The layout drops hidden widgets from its size hint, but only once it has been - // re-run: collapsing hides the rows before Qt would get around to it on its own. - layout()->activate(); + // Rows are torn down and rebuilt wholesale, and a widget added to a layout is only + // shown once that layout runs -- without this the new rows count for nothing and + // the card is measured for the context before it. The polish belongs to the same + // step: a freshly created label reports an unstyled size hint until the stylesheet + // has reached it, and the badge chips carry border and padding that change it. + m_rows->ensurePolished(); + 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_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx); if (band.width() <= 0 || band.height() <= 0) { return; } - const QSize hint = sizeHint(); - const int widthPx = qMin(hint.width(), band.width()); - const int heightPx = qMin(hint.height(), band.height()); + const int widthPx = qMin(needed.width(), band.width()); + const int heightPx = qMin(needed.height(), band.height()); // Left edge of the band, sitting on its bottom edge, so the panel grows upward as // rows are added (REQ-UI-CONTROLS-PANEL).