The panel rendered every selection out of a single pool of member widgets,
hidden and shown per branch, so each build path had to remember to hide the
other branches' widgets. That coupling produced the two defects fixed in
668ce0f, and the per-type detail the requirements now ask for would only add
more of it.
The pool is gone. SelectionPanel keeps the category arbitration, the float and
the hide-when-empty behaviour, and hosts exactly one SelectionContent at a
time; SelectionContentFactory picks which one from the selection alone. Each
card is one row of the catalog in REQ-UI-SELECTION-CONTENT and owns only its
own widgets.
The card structure (REQ-UI-SELECTION-CARD) lives in the base class: a header
with an identity symbol, a name and one right slot, then a configuration group
and a runtime group. A construction site keeps its configuration and has its
whole runtime group replaced by the construction progress, decided once there
rather than in every card (REQ-BLD-SITE-CONFIG).
Also implemented here:
- REQ-UI-SELECTION-STATUS: the header status dot, taken from the simulation's
own getProductionStatus() so the panel and the world's status light cannot
disagree.
- REQ-UI-SELECTION-AGGREGATE: belt-subsystem tiles and debris-only selections
collapse into one card with a count instead of a count summary.
- REQ-UI-HQ-PANEL: the HQ shows the global block stock and its HP, neither of
which is a buffer.
- BuildingIconCache, extracted from BuildButtonBar's file-local chip loading so
the card headers and the build buttons rasterize the same SVGs once.
FieldSelectionPanel is deleted: ships, stations, debris and the field count
summary are four more cards in the same factory, so the two-panel arbitration
collapses into one decision.
The card parts are still today's labels and buttons; the item chips, bars,
recipe summary and stat rows follow.
Build clean, 541 tests pass, app runs with no Qt warnings. Visual check
pending.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
212 lines
6.8 KiB
C++
212 lines
6.8 KiB
C++
#include "SplitterContent.h"
|
|
|
|
#include <algorithm>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <QLabel>
|
|
#include <QListWidget>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "BeltSystem.h"
|
|
#include "BuildingTarget.h"
|
|
#include "ClearBeltControl.h"
|
|
#include "Command.h"
|
|
#include "CommandRequestedEvent.h"
|
|
#include "EventManager.h"
|
|
#include "FactoryQueries.h"
|
|
#include "GameConfig.h"
|
|
#include "ItemType.h"
|
|
#include "Rotation.h"
|
|
#include "SelectionNames.h"
|
|
#include "Simulation.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Height cap on a filter list, so two of them plus the rest of the card still fit.
|
|
const int kFilterListHeightPx = 100;
|
|
|
|
QString getRotationLabel(Rotation rotation)
|
|
{
|
|
// Written as code points because the sources are read as ASCII by the compiler.
|
|
const QChar upArrow(0x2191); // U+2191 UPWARDS ARROW
|
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
|
const QChar downArrow(0x2193); // U+2193 DOWNWARDS ARROW
|
|
const QChar leftArrow(0x2190); // U+2190 LEFTWARDS ARROW
|
|
|
|
switch (rotation)
|
|
{
|
|
case Rotation::North: return QObject::tr("North (%1)").arg(upArrow);
|
|
case Rotation::East: return QObject::tr("East (%1)").arg(rightArrow);
|
|
case Rotation::South: return QObject::tr("South (%1)").arg(downArrow);
|
|
case Rotation::West: return QObject::tr("West (%1)").arg(leftArrow);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
// Every item type the economy knows, from both sides of every recipe.
|
|
std::vector<std::string> getAllItemIds(const RecipesConfig& recipes)
|
|
{
|
|
std::set<std::string> seen;
|
|
for (const RecipeDef& recipe : recipes.recipes)
|
|
{
|
|
for (const RecipeIngredient& ingredient : recipe.inputs)
|
|
{
|
|
seen.insert(ingredient.item);
|
|
}
|
|
for (const RecipeOutput& output : recipe.outputs)
|
|
{
|
|
seen.insert(output.item);
|
|
}
|
|
}
|
|
return std::vector<std::string>(seen.begin(), seen.end());
|
|
}
|
|
|
|
std::vector<ItemType> collectCheckedItems(const QListWidget* list)
|
|
{
|
|
std::vector<ItemType> filter;
|
|
for (int row = 0; row < list->count(); ++row)
|
|
{
|
|
const QListWidgetItem* item = list->item(row);
|
|
if (item->checkState() == Qt::Checked)
|
|
{
|
|
filter.push_back(ItemType{ item->text().toStdString() });
|
|
}
|
|
}
|
|
return filter;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
SplitterContent::SplitterContent(const SelectionContext& context,
|
|
const SelectionRequest& request, QWidget* parent)
|
|
: SelectionContent(context, asConstructionSite(context, request.buildings.front()),
|
|
parent)
|
|
, m_id(request.buildings.front())
|
|
, m_isSite(asConstructionSite(context, m_id).has_value())
|
|
, m_tile(0, 0)
|
|
{
|
|
m_filterALabel = new QLabel(this);
|
|
m_filterAList = new QListWidget(this);
|
|
m_filterBLabel = new QLabel(this);
|
|
m_filterBList = new QListWidget(this);
|
|
m_filterAList->setMaximumHeight(kFilterListHeightPx);
|
|
m_filterBList->setMaximumHeight(kFilterListHeightPx);
|
|
|
|
getConfigurationLayout()->addWidget(m_filterALabel);
|
|
getConfigurationLayout()->addWidget(m_filterAList);
|
|
getConfigurationLayout()->addWidget(m_filterBLabel);
|
|
getConfigurationLayout()->addWidget(m_filterBList);
|
|
|
|
getRuntimeLayout()->addWidget(
|
|
new ClearBeltControl(context, request.buildings, this));
|
|
|
|
// Populated once, here, rather than on every refresh: re-checking the boxes at 30 Hz
|
|
// would fight the player's clicks.
|
|
populateFilters();
|
|
|
|
connect(m_filterAList, &QListWidget::itemChanged,
|
|
this, [this]() { applyFilters(); });
|
|
connect(m_filterBList, &QListWidget::itemChanged,
|
|
this, [this]() { applyFilters(); });
|
|
}
|
|
|
|
void SplitterContent::refreshConfiguration()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
return;
|
|
}
|
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
}
|
|
|
|
void SplitterContent::populateFilters()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An operational splitter's outputs and filters live in the belt subsystem, keyed by
|
|
// tile; a site's are stored on the site itself (REQ-BLD-SITE-CONFIG).
|
|
std::optional<BeltSystem::SplitterInfo> info;
|
|
if (m_isSite)
|
|
{
|
|
info = getSiteSplitterInfo(getContext().sim->getFactoryState(),
|
|
*getContext().config, m_id);
|
|
}
|
|
else
|
|
{
|
|
m_tile = target.anchor;
|
|
info = getContext().sim->getBelts().getSplitterInfo(m_tile);
|
|
}
|
|
if (!info.has_value())
|
|
{
|
|
m_filterALabel->hide();
|
|
m_filterAList->hide();
|
|
m_filterBLabel->hide();
|
|
m_filterBList->hide();
|
|
return;
|
|
}
|
|
|
|
const std::vector<std::string> itemIds = getAllItemIds(getContext().config->recipes);
|
|
|
|
auto fillList = [&](QListWidget* list, QLabel* label, const QString& directionLabel,
|
|
const std::vector<ItemType>& filter)
|
|
{
|
|
label->setText(tr("%1 filter (empty = all):").arg(directionLabel));
|
|
list->blockSignals(true);
|
|
list->clear();
|
|
for (const std::string& itemId : itemIds)
|
|
{
|
|
// Only implicitly unlocked item types are offered (REQ-LOCK-UI-SPLITTER).
|
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
|
|
|
QListWidgetItem* row =
|
|
new QListWidgetItem(QString::fromStdString(itemId), list);
|
|
const bool checked = !filter.empty()
|
|
&& std::find(filter.begin(), filter.end(), ItemType{ itemId })
|
|
!= filter.end();
|
|
row->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
|
|
row->setFlags(row->flags() | Qt::ItemIsUserCheckable);
|
|
}
|
|
list->blockSignals(false);
|
|
label->show();
|
|
list->show();
|
|
};
|
|
|
|
fillList(m_filterAList, m_filterALabel, getRotationLabel(info->outputA),
|
|
info->filterA);
|
|
fillList(m_filterBList, m_filterBLabel, getRotationLabel(info->outputB),
|
|
info->filterB);
|
|
}
|
|
|
|
void SplitterContent::applyFilters()
|
|
{
|
|
if (m_isSite)
|
|
{
|
|
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
|
std::make_shared<SetSiteSplitterFiltersCommand>();
|
|
command->id = m_id;
|
|
command->filterA = collectCheckedItems(m_filterAList);
|
|
command->filterB = collectCheckedItems(m_filterBList);
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
return;
|
|
}
|
|
|
|
std::shared_ptr<SetSplitterFiltersCommand> command =
|
|
std::make_shared<SetSplitterFiltersCommand>();
|
|
command->tile = m_tile;
|
|
command->filterA = collectCheckedItems(m_filterAList);
|
|
command->filterB = collectCheckedItems(m_filterBList);
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|