Fix recipe button unclickable on construction site during play

A selected construction site rebuilt the entire panel on every
TickAdvancedEvent (~30x/s at 1x), because refreshSelectionDisplay() called
rebuild() for sites. Each rebuild runs buildSingle() -> hideAllWidgets(),
which hides and re-shows the recipe-select button. Hiding a QPushButton
mid-press clears its pressed state, so a rebuild landing between the user's
mouse press and release cancelled the click. While paused no tick advances,
so no rebuild occurred and the button worked -- matching the report.

Give sites a lightweight per-tick refresh that updates only the progress
label, mirroring refreshBuffers() for live buildings. The site progress
block is extracted from buildSingle() into refreshSiteProgress() (no
duplicated arithmetic). refreshSelectionDisplay() now takes a RefreshReason:
a PeriodicTick updates progress only, while a CommandApplied still rebuilds
so a site's newly chosen recipe/layout is reflected. The site -> completed
building transition remains handled by the existing "(Building) " title
branch.

No UI test added: the test target links only lib (no QtWidgets), per the
simulation/presentation split, so a widget-level test does not fit the
harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyCu8vwChKMbLJQ3xosYEN
This commit is contained in:
2026-07-08 20:18:24 +02:00
committed by mlangkabel
parent 94b5d941ba
commit 92ab1dab54
2 changed files with 58 additions and 31 deletions

View File

@@ -338,32 +338,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
if (m_singleIsSite)
{
QString progress;
if (s->completesAt == 0)
{
progress = tr("Queued");
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct);
}
else
{
progress = tr("Building...");
}
}
m_buffersLabel->setText(progress);
refreshSiteProgress(s);
}
else
{
@@ -371,6 +346,36 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
}
}
void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
{
QString progress;
if (s->completesAt == 0)
{
progress = tr("Queued");
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct);
}
else
{
progress = tr("Building...");
}
}
m_buffersLabel->setText(progress);
}
void SelectedBuildingPanel::refreshBuffers(const Building* b)
{
const RecipeDef* recipe = findRecipe(b);
@@ -562,7 +567,7 @@ const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{
refreshSelectionDisplay();
refreshSelectionDisplay(RefreshReason::PeriodicTick);
}
void SelectedBuildingPanel::handleEvent(
@@ -572,10 +577,10 @@ void SelectedBuildingPanel::handleEvent(
// queued drain, not synchronously. When the game is paused no tick advances,
// so TickAdvancedEvent never fires; refresh here too, otherwise the panel
// would not reflect the change until the next tick or a re-selection.
refreshSelectionDisplay();
refreshSelectionDisplay(RefreshReason::CommandApplied);
}
void SelectedBuildingPanel::refreshSelectionDisplay()
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
{
if (m_selectedEntity.has_value())
{
@@ -600,7 +605,18 @@ void SelectedBuildingPanel::refreshSelectionDisplay()
const ConstructionSite* s = m_sim->buildings().findSite(m_singleBuildingId);
if (s)
{
rebuild();
// A periodic tick only advances construction progress, so update just the
// progress label. Rebuilding every tick would hide/re-show all widgets and
// cancel any in-progress click on the recipe button. An applied command
// may have changed the site's recipe/layout, so rebuild in that case.
if (reason == RefreshReason::CommandApplied)
{
rebuild();
}
else
{
refreshSiteProgress(s);
}
return;
}
buildEmpty();

View File

@@ -59,8 +59,18 @@ private slots:
void onSplitterFilterChanged();
private:
// Why the selection display is being refreshed. A periodic tick only needs a
// lightweight content update (e.g. a construction site's progress label),
// whereas an applied player command may have changed the configuration and
// needs a full structural rebuild.
enum class RefreshReason
{
PeriodicTick,
CommandApplied
};
void onSelectionChanged(const std::vector<BuildingId>& ids);
void refreshSelectionDisplay();
void refreshSelectionDisplay(RefreshReason reason);
void rebuild();
void hideAllWidgets();
void clearContent();
@@ -68,6 +78,7 @@ private:
void buildSingle(BuildingId id);
void buildMulti(const std::vector<BuildingId>& ids);
void refreshBuffers(const Building* b);
void refreshSiteProgress(const ConstructionSite* s);
void updateShipyardLayoutWidgets(BuildingType type,
const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout);