From 9f574fa05e0aa986960539cfee42abab2d4e8896 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Fri, 7 Aug 2026 20:59:55 +0200 Subject: [PATCH] fix issue where the selection panel sometimes collapses to very small size --- src/ui/SelectionPanel.cpp | 106 ++++++++++++++++++-------- src/ui/selection/ItemChipRow.cpp | 5 ++ src/ui/selection/RecipeSummaryRow.cpp | 24 ++++-- 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/src/ui/SelectionPanel.cpp b/src/ui/SelectionPanel.cpp index aa791d2..59d7b0a 100644 --- a/src/ui/SelectionPanel.cpp +++ b/src/ui/SelectionPanel.cpp @@ -62,7 +62,13 @@ SelectionPanel::SelectionPanel(Simulation* sim, const GameConfig* config, m_scrollArea->setFrameShape(QFrame::NoFrame); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + // The panel works out for itself whether the card fits the band, and sizes itself to + // leave room for the bar when it does not (REQ-UI-SELECTION-PANEL), so refit() sets + // this policy rather than leaving the scroll area to decide. Asked to decide, it + // shows a bar the moment the card is momentarily larger than the viewport -- which + // happens while the card is being measured -- and does not take it back when the + // range turns out to be empty. + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_scrollArea->viewport()->setAutoFillBackground(false); m_body->setAutoFillBackground(false); m_scrollArea->setWidget(m_body); @@ -193,6 +199,13 @@ void SelectionPanel::rebuildContent() if (m_content) { m_bodyLayout->addWidget(m_content); + // The show is what makes the card count. A widget created under an + // already-visible parent starts hidden, and a layout treats a hidden item as + // empty -- it adds nothing to the size hint until something shows it, which + // otherwise does not happen until the event loop next runs, long after refit() + // has measured the panel. The panel then fits itself to an empty body and + // collapses to its scroll bar. + m_content->show(); m_content->refresh(); } updateVisibility(); @@ -214,11 +227,6 @@ void SelectionPanel::updateVisibility() void SelectionPanel::refit() { - // The layout drops hidden widgets from its size hint, but only once it has been - // re-run: a card hides and shows its parts as it refreshes, before Qt would get - // around to it on its own. - m_body->layout()->activate(); - const QRect band = m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx); @@ -234,34 +242,70 @@ void SelectionPanel::refit() return; } - int contentWidthPx = qMin(m_body->sizeHint().width(), maxWidthPx); - // Word-wrapped labels only know their height once the width is fixed; the layout - // reports -1 when nothing in it wraps, in which case the plain hint is exact. - int contentHeightPx = m_body->heightForWidth(contentWidthPx); - if (contentHeightPx < 0) + // What the card asks for at a given width. The width has to be applied before + // asking, because a card's height depends on the room it is given -- and so, once + // laid out, does the width it reports. Measuring at whatever width the panel + // happens to have carries the previous card's shape into this one. + // + // Each measurement re-runs the body layout. Cards are built and discarded whole, so + // its cached hint describes the card before this one until it is invalidated, and + // re-running it is also what accounts for the parts a card hides and shows as it + // refreshes. The polish belongs to the same step: a freshly created chip reports an + // unstyled hint until the stylesheet has reached it, and the chips carry border and + // padding that change their size. + auto measureAt = [this](int widthPx) -> QSize { - contentHeightPx = m_body->sizeHint().height(); - } + m_body->resize(widthPx, m_body->height()); + m_body->ensurePolished(); + m_body->layout()->invalidate(); + m_body->layout()->activate(); + return m_body->sizeHint(); + }; - if (contentHeightPx > maxHeightPx) + // Run twice. Parts of a card report an unstyled size until the style has actually + // reached them, which for a freshly built card happens during the first round of + // measuring; the second round then measures a card that is fully laid out and + // settles on the answer. Without it a card can end up a few pixels short of what it + // turns out to need, and the difference shows as a scroll bar over a card that + // looks like it fits. + for (int pass = 0; pass < 2; ++pass) { + // First at the cap, the most room the card can ever get, to learn how wide it + // wants to be; then at that width for the height that follows from it. + int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx); + int contentHeightPx = measureAt(contentWidthPx).height(); + // A card taller than the band is capped there and scrolls - // (REQ-UI-SELECTION-PANEL). The scroll bar is laid out beside the card, so the - // panel widens by its width to keep the card as wide as the height was computed - // for. - contentHeightPx = maxHeightPx; - contentWidthPx = qMin( - contentWidthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(), - maxWidthPx); + // (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel + // widens by its width to leave the card the width its height was measured for -- + // and where the cap does not allow that, the card is measured again at what is + // left over. + const bool scrolls = (contentHeightPx > maxHeightPx); + int viewportWidthPx = contentWidthPx; + if (scrolls) + { + const int scrollBarWidthPx = + m_scrollArea->verticalScrollBar()->sizeHint().width(); + contentWidthPx = qMin(contentWidthPx + scrollBarWidthPx, maxWidthPx); + contentHeightPx = maxHeightPx; + viewportWidthPx = contentWidthPx - scrollBarWidthPx; + measureAt(viewportWidthPx); + } + m_scrollArea->setVerticalScrollBarPolicy( + scrolls ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); + + // Left at the size the scroll area is about to give it, so the card is not + // briefly wider than its viewport. + m_body->resize(viewportWidthPx, m_body->sizeHint().height()); + + const int panelWidthPx = contentWidthPx + 2 * borderPx; + const int panelHeightPx = contentHeightPx + 2 * borderPx; + + // Right-aligned in the band and centered on it vertically. The band excludes the + // build button bar's strip, so centering here never puts the panel over the bar + // (REQ-UI-SELECTION-PANEL). + setGeometry(band.right() - panelWidthPx + 1, + band.top() + (band.height() - panelHeightPx) / 2, + panelWidthPx, panelHeightPx); } - - const int panelWidthPx = contentWidthPx + 2 * borderPx; - const int panelHeightPx = contentHeightPx + 2 * borderPx; - - // Right-aligned in the band and centered on it vertically. The band excludes the - // build button bar's strip, so centering here never puts the panel over the bar - // (REQ-UI-SELECTION-PANEL). - setGeometry(band.right() - panelWidthPx + 1, - band.top() + (band.height() - panelHeightPx) / 2, - panelWidthPx, panelHeightPx); } diff --git a/src/ui/selection/ItemChipRow.cpp b/src/ui/selection/ItemChipRow.cpp index bccf8a8..7501458 100644 --- a/src/ui/selection/ItemChipRow.cpp +++ b/src/ui/selection/ItemChipRow.cpp @@ -73,6 +73,11 @@ void ItemChipRow::rebuildChips(const std::vector& entries) ItemChip* chip = new ItemChip(icon, this); m_layout->addWidget(chip, static_cast(index) / kChipsPerRow, static_cast(index) % kChipsPerRow); + // Shown right away, because the panel measures itself as soon as this returns. A + // widget created under an already-visible parent starts hidden, and a layout + // treats a hidden item as empty, so an unshown chip would add nothing to the + // size hint and the panel would be fitted to a buffer section that looks empty. + chip->show(); m_chips.push_back(chip); } } diff --git a/src/ui/selection/RecipeSummaryRow.cpp b/src/ui/selection/RecipeSummaryRow.cpp index 37e3369..ecf85b5 100644 --- a/src/ui/selection/RecipeSummaryRow.cpp +++ b/src/ui/selection/RecipeSummaryRow.cpp @@ -13,6 +13,18 @@ namespace // Size the item icons are drawn at on the summary line, in device-independent pixels. const int kSummaryIconSizePx = 14; +// Adds a freshly built label to the summary 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, so an unshown label would +// add nothing to the size hint the panel measures itself against as soon as this +// returns. +void addAndShow(QHBoxLayout* layout, QLabel* label) +{ + layout->addWidget(label); + label->show(); +} + } // namespace @@ -71,14 +83,14 @@ void RecipeSummaryRow::rebuild(const std::vector& inputs, if (!inputs.empty()) { const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW - m_layout->addWidget(new QLabel(QString(rightArrow), this)); + addAndShow(m_layout, new QLabel(QString(rightArrow), this)); } addAmounts(outputs); if (durationSeconds > 0.0) { const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT - m_layout->addWidget(new QLabel( + addAndShow(m_layout, new QLabel( QStringLiteral("%1 %2").arg(middleDot).arg( tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this)); } @@ -96,13 +108,13 @@ void RecipeSummaryRow::addAmounts(const std::vector& amounts) QLabel* iconLabel = new QLabel(this); iconLabel->setPixmap( m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx)); - m_layout->addWidget(iconLabel); + addAndShow(m_layout, iconLabel); } else { - m_layout->addWidget( - new QLabel(QString::fromStdString(entry.itemId), this)); + addAndShow(m_layout, + new QLabel(QString::fromStdString(entry.itemId), this)); } - m_layout->addWidget(new QLabel(QString::number(entry.amount), this)); + addAndShow(m_layout, new QLabel(QString::number(entry.amount), this)); } }