#include "RecipeSelectionDialog.h" #include #include #include #include #include #include #include #include "Building.h" #include "BuildingType.h" #include "DisplayName.h" #include "GameConfig.h" #include "OptionButton.h" #include "RecipesConfig.h" #include "ShipsConfig.h" #include "Simulation.h" namespace { std::vector toAmounts( const std::vector& ingredients) { std::vector amounts; amounts.reserve(ingredients.size()); for (const RecipeIngredient& ingredient : ingredients) { amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount }); } return amounts; } } // namespace std::vector buildRecipeSelectionOptions( BuildingType type, Simulation& sim, const GameConfig& config) { std::vector options; // The clearing option. On a building that picks its own recipe it does not leave the // building idle but hands it back to that selection, and says so // (REQ-UI-SELECT-OPTIONS, REQ-BLD-AUTO-RECIPE). options.push_back({std::string(), isAutoRecipeBuildingType(type) ? QObject::tr("(Auto)") : QObject::tr("(None)"), RecipeLineRow::Spec()}); if (type == BuildingType::Shipyard) { for (const ShipDef& def : config.ships.ships) { if (!sim.isSchematicUnlocked(def.id)) { continue; } // The base materials and base production time, excluding the modules the // player has not placed yet (REQ-UI-SELECT-OPTIONS). No output item: a ship // has no icon, so the caption is what the option produces. RecipeLineRow::Spec line; line.inputs = toAmounts(def.schematic.materials); line.durationSeconds = def.schematic.productionTimeSeconds; options.push_back({def.id, QString::fromStdString(toDisplayName(def.id)), line}); } } else { for (const RecipeDef& recipe : config.recipes.recipes) { if (recipe.building != type) { continue; } if ((type == BuildingType::Miner || type == BuildingType::Assembler) && !sim.isRecipeUnlocked(recipe.id)) { continue; } RecipeLineRow::Spec line; line.inputs = toAmounts(recipe.inputs); line.outputGroups = RecipeLineRow::toOutputGroups(recipe); line.durationSeconds = recipe.durationSeconds; options.push_back({recipe.id, QString::fromStdString(toDisplayName(recipe.id)), line}); } } return options; } namespace { // Height the option list is allowed to take, as the dialog's parent window's height // less what the window margins and the dialog's own frame need. Fully unlocked, an // Assembler offers more options than any window can hold, and the modal is never // resized to fit (REQ-UI-PANEL-MODAL) -- so the list scrolls instead. const int kListHeightReservePx = 80; // Fallback cap for a dialog built without a parent window to measure. const int kFallbackListHeightPx = 600; } RecipeSelectionDialog::RecipeSelectionDialog( const std::vector& options, const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons, QWidget* parent) : QDialog(parent) { setWindowTitle(title); setModal(true); QVBoxLayout* mainLayout = new QVBoxLayout(this); // One vertical column of buttons, each stating what it makes (REQ-UI-SELECT-OPTIONS). QScrollArea* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); scrollArea->setMaximumHeight(parent != nullptr ? qMax(kListHeightReservePx, parent->height() - kListHeightReservePx) : kFallbackListHeightPx); mainLayout->addWidget(scrollArea); QWidget* list = new QWidget(scrollArea); QVBoxLayout* listLayout = new QVBoxLayout(list); listLayout->setContentsMargins(0, 0, 0, 0); listLayout->setSpacing(4); for (int i = 0; i < static_cast(options.size()); ++i) { const RecipeSelectionOption& option = options[static_cast(i)]; // An OptionButton so the face of widgets below is what sizes the button // (REQ-UI-SELECT-OPTIONS). OptionButton* button = new OptionButton(list); if (option.line.isEmpty()) { // The "(None)" option makes nothing and is a caption alone // (REQ-UI-SELECT-OPTIONS). button->setText(option.caption); } else { // The name over the recipe it stands for. Both are transparent to the mouse // so a click anywhere on the face still reaches the button beneath them. QVBoxLayout* face = new QVBoxLayout(button); face->setContentsMargins(8, 6, 8, 6); face->setSpacing(2); QLabel* nameLabel = new QLabel(option.caption, button); QFont nameFont = nameLabel->font(); nameFont.setBold(true); nameLabel->setFont(nameFont); nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); face->addWidget(nameLabel); RecipeLineRow* line = new RecipeLineRow(itemIcons, buildingIcons, button); line->setAttribute(Qt::WA_TransparentForMouseEvents, true); face->addWidget(line); line->setLine(option.line); } listLayout->addWidget(button); m_optionIds.push_back(option.id); const int index = i; connect(button, &QPushButton::clicked, this, [this, index]() { onOptionClicked(index); }); } listLayout->addStretch(1); scrollArea->setWidget(list); } std::optional RecipeSelectionDialog::getChosenId() const { return m_chosenId; } void RecipeSelectionDialog::onOptionClicked(int index) { if (index >= 0 && index < static_cast(m_optionIds.size())) { m_chosenId = m_optionIds[static_cast(index)]; } accept(); }