From bdc1be25aff5b9c828617e14d247ef81e7a0fecb Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Wed, 8 Jul 2026 20:18:24 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01DyCu8vwChKMbLJQ3xosYEN --- src/ui/SelectedBuildingPanel.cpp | 76 +++++++++++++++++++------------- src/ui/SelectedBuildingPanel.h | 13 +++++- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/ui/SelectedBuildingPanel.cpp b/src/ui/SelectedBuildingPanel.cpp index bc28196..d63a716 100644 --- a/src/ui/SelectedBuildingPanel.cpp +++ b/src/ui/SelectedBuildingPanel.cpp @@ -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( - 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( + 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 /*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(); diff --git a/src/ui/SelectedBuildingPanel.h b/src/ui/SelectedBuildingPanel.h index 35f514d..1aac58a 100644 --- a/src/ui/SelectedBuildingPanel.h +++ b/src/ui/SelectedBuildingPanel.h @@ -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& 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& ids); void refreshBuffers(const Building* b); + void refreshSiteProgress(const ConstructionSite* s); void updateShipyardLayoutWidgets(BuildingType type, const std::string& recipeId, const std::optional& shipLayout);