size the controls panel to the rows it is actually showing

The card was drawn one rebuild behind: selecting something for the first time
sized it for the context before it, and the next selection sized it for that
one. refit() activated the panel's outer layout but never the rows layout
underneath it, so it read a size hint describing rows that were no longer
there, while the freshly added ones were not yet shown and so counted for
nothing. Both layouts are now invalidated and re-run innermost first, and the
rows are polished before being measured -- the badge chips carry border and
padding, which a label reports only once the stylesheet has reached it.

Also drops letter-spacing from the stylesheet. Qt has no such property and
warned once per widget it was applied to, which was most of the console. The
heading and the caption set it on their QFont, where it works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
2026-08-07 19:21:28 +02:00
parent b634da70fb
commit b346f3dfc1

View File

@@ -1,5 +1,6 @@
#include "ControlsPanel.h" #include "ControlsPanel.h"
#include <QFont>
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
@@ -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". // Separates the heading's name from its detail, e.g. "BUILD MODE * Assembler".
const QChar kHeadingSeparator(0x00B7); // U+00B7 MIDDLE DOT 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 // 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. // 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) QWidget* makeRow(ControlAction action, const ControlContext& context, QWidget* parent)
@@ -63,19 +78,20 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
// class scoped selector keeps the border on the panel rather than cascading onto // class scoped selector keeps the border on the panel rather than cascading onto
// its children. // its children.
setAttribute(Qt::WA_StyledBackground, true); 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( setStyleSheet(QStringLiteral(
"ControlsPanel { background-color: palette(window);" "ControlsPanel { background-color: palette(window);"
" border: 1px solid palette(mid); border-radius: 4px; }" " border: 1px solid palette(mid); border-radius: 4px; }"
"QLabel#controlHeading { font-weight: bold; letter-spacing: 1px;" "QLabel#controlHeading { color: palette(text); }"
" color: palette(text); }"
"QLabel#controlBadge { border: 1px solid palette(mid); border-radius: 3px;" "QLabel#controlBadge { border: 1px solid palette(mid); border-radius: 3px;"
" padding: 1px 5px; font-family: monospace; color: palette(text); }" " padding: 1px 5px; font-family: monospace; color: palette(text); }"
"QLabel#controlLabel { color: palette(text); }" "QLabel#controlLabel { color: palette(text); }"
// The row that leaves the mode reads differently from the ones that act within // The row that leaves the mode reads differently from the ones that act within
// it (REQ-UI-CONTROLS-CARD). // it (REQ-UI-CONTROLS-CARD).
"QLabel#controlLabelExit { color: palette(bright-text); }" "QLabel#controlLabelExit { color: palette(bright-text); }"
"QLabel#controlCaption { color: palette(mid); font-size: 10px;" "QLabel#controlCaption { color: palette(mid); }"));
" letter-spacing: 1px; }"));
QVBoxLayout* outerLayout = new QVBoxLayout(this); QVBoxLayout* outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx, outerLayout->setContentsMargins(kCardMarginPx, kCardMarginPx,
@@ -85,6 +101,7 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
m_heading = new QLabel(this); m_heading = new QLabel(this);
m_heading->setObjectName(QStringLiteral("controlHeading")); m_heading->setObjectName(QStringLiteral("controlHeading"));
m_heading->setCursor(Qt::PointingHandCursor); m_heading->setCursor(Qt::PointingHandCursor);
m_heading->setFont(makeSpacedFont(font(), /*bold*/ true, /*pointSizeDelta*/ 0));
outerLayout->addWidget(m_heading); outerLayout->addWidget(m_heading);
m_rows = new QWidget(this); m_rows = new QWidget(this);
@@ -172,6 +189,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
QLabel* caption = new QLabel(tr("ALWAYS AVAILABLE"), m_rows); QLabel* caption = new QLabel(tr("ALWAYS AVAILABLE"), m_rows);
caption->setObjectName(QStringLiteral("controlCaption")); caption->setObjectName(QStringLiteral("controlCaption"));
caption->setFont(makeSpacedFont(font(), /*bold*/ false, /*pointSizeDelta*/ -1));
m_rowsLayout->addWidget(caption); m_rowsLayout->addWidget(caption);
} }
@@ -221,8 +239,19 @@ void ControlsPanel::refit()
{ {
if (m_bandRect.isNull()) { return; } if (m_bandRect.isNull()) { return; }
// The layout drops hidden widgets from its size hint, but only once it has been // Both layouts, innermost first, and both invalidated before being re-run. Rows are
// re-run: collapsing hides the rows before Qt would get around to it on its own. // created and destroyed wholesale, so the rows layout's cached size hint describes
// the previous context until it is discarded; asking the panel for its hint without
// this returns the size the last context needed, which is why a first selection
// used to draw a card one rebuild behind.
//
// The polish is part of the same problem: a freshly created label reports an
// unstyled size hint until the stylesheet has been applied to it, and the badge
// chips carry border and padding that change their width.
m_rows->ensurePolished();
m_rowsLayout->invalidate();
m_rowsLayout->activate();
layout()->invalidate();
layout()->activate(); layout()->activate();
const QRect band = m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx); const QRect band = m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);