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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user