split the selection panel into one card per kind of selection
This commit is contained in:
254
src/ui/selection/SelectionContent.cpp
Normal file
254
src/ui/selection/SelectionContent.cpp
Normal file
@@ -0,0 +1,254 @@
|
||||
#include "SelectionContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QRectF>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BuildingIconCache.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size of the identity chip in the card header, in device-independent pixels. Smaller
|
||||
// than the build button's 32 px chip: here it labels a line of text rather than being
|
||||
// the whole button face.
|
||||
const int kSymbolSizePx = 20;
|
||||
|
||||
// Diameter of the status dot beside the header's right-slot caption
|
||||
// (REQ-UI-SELECTION-STATUS).
|
||||
const int kStatusDotSizePx = 8;
|
||||
|
||||
// Spacing inside the card and between the header's elements.
|
||||
const int kCardSpacingPx = 6;
|
||||
const int kHeaderSpacingPx = 6;
|
||||
|
||||
QPixmap renderStatusDot(const QColor& fill, const QColor& outline)
|
||||
{
|
||||
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||
QPixmap pixmap(static_cast<int>(kStatusDotSizePx * dpr),
|
||||
static_cast<int>(kStatusDotSizePx * dpr));
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setPen(outline);
|
||||
painter.setBrush(fill);
|
||||
// Inset by half the pen width so the outline stays inside the pixmap.
|
||||
painter.drawEllipse(QRectF(0.5, 0.5, kStatusDotSizePx - 1.0, kStatusDotSizePx - 1.0));
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
SelectionContent::SelectionContent(const SelectionContext& context,
|
||||
std::optional<BuildingId> constructionSiteId,
|
||||
QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_context(context)
|
||||
, m_siteId(constructionSiteId)
|
||||
, m_constructionLabel(nullptr)
|
||||
{
|
||||
QVBoxLayout* cardLayout = new QVBoxLayout(this);
|
||||
cardLayout->setContentsMargins(0, 0, 0, 0);
|
||||
cardLayout->setSpacing(kCardSpacingPx);
|
||||
|
||||
// Header: identity symbol, name, and the right slot pushed to the far edge
|
||||
// (REQ-UI-SELECTION-CARD).
|
||||
QWidget* header = new QWidget(this);
|
||||
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
||||
headerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
headerLayout->setSpacing(kHeaderSpacingPx);
|
||||
|
||||
m_symbolLabel = new QLabel(header);
|
||||
m_symbolLabel->hide();
|
||||
|
||||
m_nameLabel = new QLabel(header);
|
||||
QFont nameFont = m_nameLabel->font();
|
||||
nameFont.setBold(true);
|
||||
m_nameLabel->setFont(nameFont);
|
||||
|
||||
m_slotDot = new QLabel(header);
|
||||
m_slotDot->hide();
|
||||
|
||||
m_slotLabel = new QLabel(header);
|
||||
m_slotLabel->hide();
|
||||
|
||||
headerLayout->addWidget(m_symbolLabel);
|
||||
headerLayout->addWidget(m_nameLabel);
|
||||
headerLayout->addStretch(1);
|
||||
headerLayout->addWidget(m_slotDot);
|
||||
headerLayout->addWidget(m_slotLabel);
|
||||
cardLayout->addWidget(header);
|
||||
|
||||
m_configurationGroup = new QWidget(this);
|
||||
QVBoxLayout* configurationLayout = new QVBoxLayout(m_configurationGroup);
|
||||
configurationLayout->setContentsMargins(0, 0, 0, 0);
|
||||
configurationLayout->setSpacing(kCardSpacingPx);
|
||||
cardLayout->addWidget(m_configurationGroup);
|
||||
|
||||
m_runtimeGroup = new QWidget(this);
|
||||
QVBoxLayout* runtimeLayout = new QVBoxLayout(m_runtimeGroup);
|
||||
runtimeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
runtimeLayout->setSpacing(kCardSpacingPx);
|
||||
cardLayout->addWidget(m_runtimeGroup);
|
||||
|
||||
if (m_siteId.has_value())
|
||||
{
|
||||
// A site has no buffers and runs no production cycle, so whatever the subclass
|
||||
// puts into the runtime group is not shown at all; the construction section
|
||||
// takes its place (REQ-BLD-SITE-CONFIG, REQ-UI-SELECTION-CARD). The subclass
|
||||
// still fills the group -- it just never becomes visible.
|
||||
m_runtimeGroup->hide();
|
||||
m_constructionLabel = new QLabel(this);
|
||||
cardLayout->addWidget(m_constructionLabel);
|
||||
|
||||
setSlot(QColor(), tr("constructing"));
|
||||
}
|
||||
}
|
||||
|
||||
SelectionContent::~SelectionContent() = default;
|
||||
|
||||
void SelectionContent::refresh()
|
||||
{
|
||||
refreshConfiguration();
|
||||
if (m_siteId.has_value())
|
||||
{
|
||||
refreshConstruction();
|
||||
return;
|
||||
}
|
||||
refreshRuntime();
|
||||
}
|
||||
|
||||
QVBoxLayout* SelectionContent::getConfigurationLayout()
|
||||
{
|
||||
return static_cast<QVBoxLayout*>(m_configurationGroup->layout());
|
||||
}
|
||||
|
||||
QVBoxLayout* SelectionContent::getRuntimeLayout()
|
||||
{
|
||||
return static_cast<QVBoxLayout*>(m_runtimeGroup->layout());
|
||||
}
|
||||
|
||||
void SelectionContent::setIdentity(const QPixmap& symbol, const QString& name)
|
||||
{
|
||||
m_symbolLabel->setPixmap(symbol);
|
||||
m_symbolLabel->setVisible(!symbol.isNull());
|
||||
m_nameLabel->setText(name);
|
||||
}
|
||||
|
||||
void SelectionContent::setBuildingIdentity(BuildingType type, const QString& name)
|
||||
{
|
||||
const std::string iconName = buildingTypeId(type);
|
||||
setIdentity(m_context.buildingIcons->getChip(iconName, kSymbolSizePx), name);
|
||||
}
|
||||
|
||||
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
|
||||
{
|
||||
if (dotColor.isValid())
|
||||
{
|
||||
m_slotDot->setPixmap(
|
||||
renderStatusDot(dotColor, m_context.visuals->statusLight.outline));
|
||||
m_slotDot->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_slotDot->hide();
|
||||
}
|
||||
m_slotLabel->setText(caption);
|
||||
m_slotLabel->setVisible(!caption.isEmpty());
|
||||
}
|
||||
|
||||
void SelectionContent::setCountSlot(int count)
|
||||
{
|
||||
setSlot(QColor(), tr("x%1").arg(count));
|
||||
}
|
||||
|
||||
void SelectionContent::clearSlot()
|
||||
{
|
||||
setSlot(QColor(), QString());
|
||||
}
|
||||
|
||||
void SelectionContent::setProductionStatusSlot(const Building& building)
|
||||
{
|
||||
// The classification is the simulation's, so the panel and the world's status light
|
||||
// can never disagree (REQ-UI-SELECTION-STATUS, REQ-UI-STATUS-LIGHT).
|
||||
const std::optional<ProductionStatus> status =
|
||||
getProductionStatus(*m_context.config, building);
|
||||
if (!status.has_value())
|
||||
{
|
||||
clearSlot();
|
||||
return;
|
||||
}
|
||||
|
||||
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
||||
// The Salvage Bay has no recipe and no cycle: its two states say whether it is
|
||||
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
|
||||
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
||||
switch (*status)
|
||||
{
|
||||
case ProductionStatus::Unconfigured:
|
||||
setSlot(colors.grey, tr("no recipe"));
|
||||
break;
|
||||
case ProductionStatus::Producing:
|
||||
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
|
||||
break;
|
||||
case ProductionStatus::Starved:
|
||||
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
|
||||
break;
|
||||
case ProductionStatus::Blocked:
|
||||
setSlot(colors.yellow, tr("output full"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectionContent::refreshConstruction()
|
||||
{
|
||||
const ConstructionSite* site =
|
||||
findSite(m_context.sim->getFactoryState(), *m_siteId);
|
||||
if (!site)
|
||||
{
|
||||
// The site finished or was removed under the card. SelectionPanel rebuilds on
|
||||
// the same refresh, so this only has to avoid reading a dead site.
|
||||
return;
|
||||
}
|
||||
|
||||
QString progress;
|
||||
if (site->completesAt == 0)
|
||||
{
|
||||
progress = tr("Queued");
|
||||
}
|
||||
else
|
||||
{
|
||||
const BuildingDef* def =
|
||||
m_context.config->buildings.findBuildingDef(site->type);
|
||||
if (def && def->constructionTimeSeconds > 0)
|
||||
{
|
||||
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||
const Tick elapsed =
|
||||
m_context.sim->getCurrentTick() - (site->completesAt - duration);
|
||||
const int percent = static_cast<int>(
|
||||
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
|
||||
progress = tr("Construction: %1%").arg(percent);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = tr("Building...");
|
||||
}
|
||||
}
|
||||
m_constructionLabel->setText(progress);
|
||||
}
|
||||
Reference in New Issue
Block a user