split the selection panel into one card per kind of selection
This commit is contained in:
211
src/ui/selection/SplitterContent.cpp
Normal file
211
src/ui/selection/SplitterContent.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#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));
|
||||
}
|
||||
Reference in New Issue
Block a user