show recipes visually instead of describing them in text
This commit is contained in:
@@ -2,60 +2,46 @@
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QFont>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingType.h"
|
||||
#include "DisplayName.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "OptionButton.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "RecipeTooltip.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QString itemLine(const std::string& itemId, int amount)
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(
|
||||
const std::vector<RecipeIngredient>& ingredients)
|
||||
{
|
||||
return QStringLiteral(" ")
|
||||
+ QString::fromStdString(toDisplayName(itemId))
|
||||
+ QStringLiteral(" x ") + QString::number(amount);
|
||||
std::vector<RecipeLineRow::Amount> amounts;
|
||||
amounts.reserve(ingredients.size());
|
||||
for (const RecipeIngredient& ingredient : ingredients)
|
||||
{
|
||||
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
QString shipTooltip(const ShipDef& def)
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
|
||||
{
|
||||
const QString name = QString::fromStdString(toDisplayName(def.id));
|
||||
|
||||
QStringList lines;
|
||||
lines << QObject::tr("Ship: %1").arg(name);
|
||||
|
||||
if (def.schematic.materials.empty())
|
||||
std::vector<RecipeLineRow::Amount> amounts;
|
||||
amounts.reserve(outputs.size());
|
||||
for (const RecipeOutput& output : outputs)
|
||||
{
|
||||
lines << QObject::tr("Materials: none");
|
||||
amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
|
||||
}
|
||||
else
|
||||
{
|
||||
lines << QObject::tr("Materials:");
|
||||
for (const RecipeIngredient& material : def.schematic.materials)
|
||||
{
|
||||
lines << itemLine(material.item, material.amount);
|
||||
}
|
||||
lines << QObject::tr(" + installed modules");
|
||||
}
|
||||
|
||||
lines << QObject::tr("Completion time: %1 s")
|
||||
.arg(def.schematic.productionTimeSeconds);
|
||||
lines << QObject::tr("Produces: 1 %1").arg(name);
|
||||
|
||||
return lines.join('\n');
|
||||
return amounts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -64,16 +50,24 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
||||
BuildingType type, Simulation& sim, const GameConfig& config)
|
||||
{
|
||||
std::vector<RecipeSelectionOption> options;
|
||||
options.push_back({std::string(), QObject::tr("(None)"), QString()});
|
||||
options.push_back({std::string(), 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)),
|
||||
shipTooltip(def)});
|
||||
line});
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -87,16 +81,15 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Icon shown on the option button (REQ-UI-RECIPE-ICON): the recipe's
|
||||
// configured icon item, else its first output item.
|
||||
const std::string iconItemId = recipe.icon
|
||||
? *recipe.icon
|
||||
: (recipe.outputs.empty() ? std::string()
|
||||
: recipe.outputs.front().item);
|
||||
|
||||
RecipeLineRow::Spec line;
|
||||
line.inputs = toAmounts(recipe.inputs);
|
||||
line.outputs = toAmounts(recipe.outputs);
|
||||
line.durationSeconds = recipe.durationSeconds;
|
||||
|
||||
options.push_back({recipe.id,
|
||||
QString::fromStdString(toDisplayName(recipe.id)),
|
||||
buildRecipeTooltip(recipe),
|
||||
iconItemId});
|
||||
line});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,51 +98,77 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
||||
|
||||
namespace
|
||||
{
|
||||
// On-screen size of a recipe option button's item icon (REQ-UI-RECIPE-ICON).
|
||||
const QSize kOptionIconSize(32, 32);
|
||||
// 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<RecipeSelectionOption>& options,
|
||||
const QString& title, ItemIconCache* itemIcons, QWidget* parent)
|
||||
const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
setModal(true);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
QGridLayout* grid = new QGridLayout();
|
||||
mainLayout->addLayout(grid);
|
||||
|
||||
const int columnCount = 2;
|
||||
// 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<int>(options.size()); ++i)
|
||||
{
|
||||
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
|
||||
|
||||
QPushButton* button = new QPushButton(this);
|
||||
// Icon-only for a recipe option: the produced item's icon on its colored square,
|
||||
// or the square alone when the item has no icon file (REQ-UI-RECIPE-ICON). The
|
||||
// name stays reachable via the tooltip. Options carrying no item at all -- the
|
||||
// "(None)" entry and the Shipyard's schematics -- keep their text caption, as
|
||||
// does an item with neither square nor icon to show.
|
||||
const QPixmap icon = option.iconItemId.empty()
|
||||
? QPixmap()
|
||||
: itemIcons->getSquarePixmap(option.iconItemId, kOptionIconSize.width());
|
||||
if (!icon.isNull())
|
||||
// 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())
|
||||
{
|
||||
button->setIcon(QIcon(icon));
|
||||
button->setIconSize(kOptionIconSize);
|
||||
// The "(None)" option makes nothing and is a caption alone
|
||||
// (REQ-UI-SELECT-OPTIONS).
|
||||
button->setText(option.caption);
|
||||
}
|
||||
else
|
||||
{
|
||||
button->setText(option.caption);
|
||||
// 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);
|
||||
}
|
||||
if (!option.tooltip.isEmpty())
|
||||
{
|
||||
button->setToolTip(option.tooltip);
|
||||
}
|
||||
button->setMinimumHeight(40);
|
||||
grid->addWidget(button, i / columnCount, i % columnCount);
|
||||
listLayout->addWidget(button);
|
||||
|
||||
m_optionIds.push_back(option.id);
|
||||
const int index = i;
|
||||
@@ -158,6 +177,9 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
onOptionClicked(index);
|
||||
});
|
||||
}
|
||||
listLayout->addStretch(1);
|
||||
|
||||
scrollArea->setWidget(list);
|
||||
}
|
||||
|
||||
std::optional<std::string> RecipeSelectionDialog::getChosenId() const
|
||||
|
||||
Reference in New Issue
Block a user