Render produced-item icons in recipe dialog and game world
Implements REQ-UI-ITEM-ICON and REQ-UI-RECIPE-ICON. Adds an optional recipes.toml `icon` field (an item id; defaults to the recipe's first output). New ItemIconCache loads per-item SVGs from icons/items and caches pixmaps per target pixel size, shared by: - the Miner/Assembler recipe-selection dialog, which now shows icon-only option buttons (name-caption fallback when no icon file exists), and - GameWorldView belt/port item rendering via a shared drawWorldItem helper (colored-square fallback preserved). Shipyard schematic dialog is unchanged. No icon art is shipped; every path falls back cleanly when an item has no SVG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -421,6 +421,14 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
|||||||
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||||
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
||||||
|
|
||||||
|
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
|
||||||
|
// in the UI when unset. Not validated against known items here — a missing
|
||||||
|
// icon is not an error (REQ-UI-ITEM-ICON).
|
||||||
|
if (mt.contains("icon"))
|
||||||
|
{
|
||||||
|
def.icon = requireString(mt["icon"], file, elemPath + ".icon");
|
||||||
|
}
|
||||||
|
|
||||||
cfg.recipes.push_back(std::move(def));
|
cfg.recipes.push_back(std::move(def));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ struct RecipeDef
|
|||||||
std::vector<RecipeIngredient> inputs;
|
std::vector<RecipeIngredient> inputs;
|
||||||
std::vector<RecipeOutput> outputs;
|
std::vector<RecipeOutput> outputs;
|
||||||
double durationSeconds;
|
double durationSeconds;
|
||||||
|
// Optional id of the item whose icon represents this recipe in the recipe-
|
||||||
|
// selection dialog (REQ-UI-RECIPE-ICON). When unset, the first output item is
|
||||||
|
// used. A missing icon file for that item is not an error (REQ-UI-ITEM-ICON).
|
||||||
|
std::optional<std::string> icon;
|
||||||
// Assembler only. When true, this recipe is available from game start
|
// Assembler only. When true, this recipe is available from game start
|
||||||
// regardless of the implicit item graph — used for base recipes that no
|
// regardless of the implicit item graph — used for base recipes that no
|
||||||
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
|
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
|
||||||
|
|||||||
@@ -390,6 +390,39 @@ duration_seconds = 1.0
|
|||||||
std::runtime_error);
|
std::runtime_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Optional recipe icon field parses; absent leaves it unset", "[config]")
|
||||||
|
{
|
||||||
|
TempConfigDir dir;
|
||||||
|
writeFile(dir.path() / "recipes.toml", R"(
|
||||||
|
[[recipe]]
|
||||||
|
id = "with_icon"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
outputs = [{item = "steel_plate", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
icon = "hardened_steel"
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "without_icon"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
outputs = [{item = "copper_wire", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
)");
|
||||||
|
|
||||||
|
const RecipesConfig cfg =
|
||||||
|
ConfigLoader::loadRecipes((dir.path() / "recipes.toml").string());
|
||||||
|
|
||||||
|
const RecipeDef& withIcon = cfg.recipes.at(0);
|
||||||
|
REQUIRE(withIcon.id == "with_icon");
|
||||||
|
REQUIRE(withIcon.icon.has_value());
|
||||||
|
REQUIRE(*withIcon.icon == "hardened_steel");
|
||||||
|
|
||||||
|
const RecipeDef& withoutIcon = cfg.recipes.at(1);
|
||||||
|
REQUIRE(withoutIcon.id == "without_icon");
|
||||||
|
REQUIRE_FALSE(withoutIcon.icon.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
// --- unlock_requires (REQ-LOCK-PREREQ) ------------------------------------
|
// --- unlock_requires (REQ-LOCK-PREREQ) ------------------------------------
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,5 +35,6 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "GameOverEvent.h"
|
#include "GameOverEvent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
@@ -222,6 +223,11 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
|||||||
|
|
||||||
loadBuildingIcons(configDir);
|
loadBuildingIcons(configDir);
|
||||||
|
|
||||||
|
// Item icons live beside the config dir, mirroring the building icons
|
||||||
|
// (REQ-UI-ITEM-ICON, REQ-UI-WORLD-ICON).
|
||||||
|
m_itemIcons = std::make_unique<ItemIconCache>(QDir::cleanPath(
|
||||||
|
QString::fromStdString(configDir) + "/../icons/items"));
|
||||||
|
|
||||||
m_renderTimer = new QTimer(this);
|
m_renderTimer = new QTimer(this);
|
||||||
m_renderTimer->setInterval(16);
|
m_renderTimer->setInterval(16);
|
||||||
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
||||||
@@ -1597,23 +1603,15 @@ void GameWorldView::drawPortItems(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared with belt items (REQ-GW-TILE-SIZE): a half-tile filled square + outline.
|
// Shared with belt items (REQ-GW-TILE-SIZE): a half-tile item icon, or the
|
||||||
|
// colored-square fallback (REQ-UI-ITEM-ICON), via the same draw path.
|
||||||
const std::function<void(const ItemType&, QPointF)> drawItem =
|
const std::function<void(const ItemType&, QPointF)> drawItem =
|
||||||
[&](const ItemType& type, QPointF worldPos)
|
[&](const ItemType& type, QPointF worldPos)
|
||||||
{
|
{
|
||||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
|
||||||
m_visuals->items.find(type.id);
|
|
||||||
if (it == m_visuals->items.end()) { return; }
|
|
||||||
|
|
||||||
const QPointF center = worldToWidget(
|
const QPointF center = worldToWidget(
|
||||||
QVector2D(static_cast<float>(worldPos.x()),
|
QVector2D(static_cast<float>(worldPos.x()),
|
||||||
static_cast<float>(worldPos.y())));
|
static_cast<float>(worldPos.y())));
|
||||||
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
|
drawWorldItem(painter, type.id, center, halfPx);
|
||||||
halfPx * 2, halfPx * 2);
|
|
||||||
painter.fillRect(itemRect, it->second.fill);
|
|
||||||
painter.setPen(QPen(it->second.outline, 1));
|
|
||||||
painter.setBrush(Qt::NoBrush);
|
|
||||||
painter.drawRect(itemRect);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
painter.save();
|
painter.save();
|
||||||
@@ -1623,6 +1621,33 @@ void GameWorldView::drawPortItems(QPainter& painter)
|
|||||||
painter.restore();
|
painter.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawWorldItem(QPainter& painter, const std::string& itemId,
|
||||||
|
QPointF center, float halfPx)
|
||||||
|
{
|
||||||
|
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
|
||||||
|
halfPx * 2, halfPx * 2);
|
||||||
|
|
||||||
|
// Prefer the item's icon (REQ-UI-ITEM-ICON); it is rasterized once at the current
|
||||||
|
// half-tile pixel size and cached, so this is a plain pixmap blit per frame.
|
||||||
|
if (m_itemIcons && m_itemIcons->hasIcon(itemId))
|
||||||
|
{
|
||||||
|
int sizePx = qRound(static_cast<qreal>(halfPx * 2.0f));
|
||||||
|
if (sizePx < 1) { sizePx = 1; }
|
||||||
|
painter.drawPixmap(itemRect, m_itemIcons->getPixmap(itemId, sizePx),
|
||||||
|
QRectF(0, 0, sizePx, sizePx));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: the colored square from visuals.toml (REQ-GW-TILE-SIZE).
|
||||||
|
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||||
|
m_visuals->items.find(itemId);
|
||||||
|
if (it == m_visuals->items.end()) { return; }
|
||||||
|
painter.fillRect(itemRect, it->second.fill);
|
||||||
|
painter.setPen(QPen(it->second.outline, 1));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(itemRect);
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::drawBeltItems(QPainter& painter)
|
void GameWorldView::drawBeltItems(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float halfPx = getTilePx() * 0.5f * 0.5f;
|
const float halfPx = getTilePx() * 0.5f * 0.5f;
|
||||||
@@ -1630,19 +1655,10 @@ void GameWorldView::drawBeltItems(QPainter& painter)
|
|||||||
|
|
||||||
m_sim->getBelts().forEachVisualItem(vr, [&](const VisualItem& vi)
|
m_sim->getBelts().forEachVisualItem(vr, [&](const VisualItem& vi)
|
||||||
{
|
{
|
||||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
|
||||||
m_visuals->items.find(vi.type.id);
|
|
||||||
if (it == m_visuals->items.end()) { return; }
|
|
||||||
|
|
||||||
const QPointF center = worldToWidget(
|
const QPointF center = worldToWidget(
|
||||||
QVector2D(static_cast<float>(vi.worldPos.x()),
|
QVector2D(static_cast<float>(vi.worldPos.x()),
|
||||||
static_cast<float>(vi.worldPos.y())));
|
static_cast<float>(vi.worldPos.y())));
|
||||||
const QRectF rect(center.x() - halfPx, center.y() - halfPx,
|
drawWorldItem(painter, vi.type.id, center, halfPx);
|
||||||
halfPx * 2, halfPx * 2);
|
|
||||||
painter.fillRect(rect, it->second.fill);
|
|
||||||
painter.setPen(QPen(it->second.outline, 1));
|
|
||||||
painter.setBrush(Qt::NoBrush);
|
|
||||||
painter.drawRect(rect);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QOpenGLWidget>
|
#include <QOpenGLWidget>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
#include <QPointF>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
|
|
||||||
struct Command;
|
struct Command;
|
||||||
struct ParsedReplay;
|
struct ParsedReplay;
|
||||||
|
class ItemIconCache;
|
||||||
class ReplayPlayer;
|
class ReplayPlayer;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
@@ -128,6 +130,12 @@ private:
|
|||||||
void drawCopyConfigFeedback(QPainter& painter);
|
void drawCopyConfigFeedback(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
void drawBeltItems(QPainter& painter);
|
void drawBeltItems(QPainter& painter);
|
||||||
|
// Draws a single item centered at widget-space `center`, spanning `halfPx` in
|
||||||
|
// each direction (a half-tile). Uses the item's icon when one exists
|
||||||
|
// (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from
|
||||||
|
// visuals.toml. Shared by drawBeltItems and drawPortItems.
|
||||||
|
void drawWorldItem(QPainter& painter, const std::string& itemId,
|
||||||
|
QPointF center, float halfPx);
|
||||||
void drawDebris(QPainter& painter);
|
void drawDebris(QPainter& painter);
|
||||||
void drawShips(QPainter& painter);
|
void drawShips(QPainter& painter);
|
||||||
void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
|
void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
|
||||||
@@ -296,6 +304,10 @@ private:
|
|||||||
};
|
};
|
||||||
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
|
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
|
||||||
|
|
||||||
|
// Per-item icon cache (REQ-UI-ITEM-ICON), loaded from <configDir>/../icons/items.
|
||||||
|
// Shared draw path for belt and port items; pixmaps are cached per target size.
|
||||||
|
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||||
|
|
||||||
// Funnels all player input into the single Simulation::apply chokepoint.
|
// Funnels all player input into the single Simulation::apply chokepoint.
|
||||||
CommandManager m_commandManager;
|
CommandManager m_commandManager;
|
||||||
// A Reset command was enqueued; reset the view after the next drain applies it.
|
// A Reset command was enqueued; reset the view after the next drain applies it.
|
||||||
|
|||||||
64
src/ui/ItemIconCache.cpp
Normal file
64
src/ui/ItemIconCache.cpp
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#include "ItemIconCache.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
|
||||||
|
ItemIconCache::ItemIconCache(const QString& iconDir)
|
||||||
|
: m_iconDir(iconDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray& ItemIconCache::getSvg(const std::string& itemId)
|
||||||
|
{
|
||||||
|
const std::map<std::string, QByteArray>::const_iterator cached =
|
||||||
|
m_svgById.find(itemId);
|
||||||
|
if (cached != m_svgById.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An absent or unreadable file caches an empty byte array so it is not retried;
|
||||||
|
// a missing icon is not an error (REQ-UI-ITEM-ICON).
|
||||||
|
QByteArray svg;
|
||||||
|
QFile file(m_iconDir + "/" + QString::fromStdString(itemId) + ".svg");
|
||||||
|
if (file.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
svg = file.readAll();
|
||||||
|
}
|
||||||
|
return m_svgById.emplace(itemId, std::move(svg)).first->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ItemIconCache::hasIcon(const std::string& itemId)
|
||||||
|
{
|
||||||
|
return !getSvg(itemId).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
|
||||||
|
{
|
||||||
|
if (sizePx <= 0)
|
||||||
|
{
|
||||||
|
return QPixmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::pair<std::string, int> key(itemId, sizePx);
|
||||||
|
const std::map<std::pair<std::string, int>, QPixmap>::const_iterator cached =
|
||||||
|
m_pixmapCache.find(key);
|
||||||
|
if (cached != m_pixmapCache.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray& svg = getSvg(itemId);
|
||||||
|
QPixmap pixmap;
|
||||||
|
if (!svg.isEmpty())
|
||||||
|
{
|
||||||
|
QSvgRenderer renderer(svg);
|
||||||
|
pixmap = QPixmap(sizePx, sizePx);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer.render(&painter);
|
||||||
|
}
|
||||||
|
return m_pixmapCache.emplace(key, std::move(pixmap)).first->second;
|
||||||
|
}
|
||||||
45
src/ui/ItemIconCache.h
Normal file
45
src/ui/ItemIconCache.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Rasterizes and caches per-item icon SVGs (REQ-UI-ITEM-ICON). Item icons are
|
||||||
|
// self-contained, full-color SVGs loaded from a directory, one file per item type
|
||||||
|
// named after the item's id (e.g. "iron_ore.svg"). Shared by the recipe-selection
|
||||||
|
// dialog (REQ-UI-RECIPE-ICON) and the game world's belt/port item rendering so the
|
||||||
|
// rasterization is not duplicated.
|
||||||
|
//
|
||||||
|
// A missing icon file is not an error: hasIcon() returns false for it and the caller
|
||||||
|
// falls back (a colored square in the world, a name caption in the dialog).
|
||||||
|
class ItemIconCache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// iconDir is the directory holding the "<item_id>.svg" icon files
|
||||||
|
// (typically "<configDir>/../icons/items").
|
||||||
|
explicit ItemIconCache(const QString& iconDir);
|
||||||
|
|
||||||
|
// True if an icon SVG file exists for the given item id. Loads the file's bytes
|
||||||
|
// on first query and remembers the result (including absence) so repeated calls
|
||||||
|
// are cheap.
|
||||||
|
bool hasIcon(const std::string& itemId);
|
||||||
|
|
||||||
|
// Returns the item's icon rasterized to a transparent sizePx*sizePx pixmap,
|
||||||
|
// cached per (item id, size) so it is rendered once and reused across frames and
|
||||||
|
// only re-rasterized when the target size changes (REQ-UI-ITEM-ICON). Returns a
|
||||||
|
// null pixmap if the item has no icon file (callers should gate on hasIcon()).
|
||||||
|
QPixmap getPixmap(const std::string& itemId, int sizePx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Returns the raw SVG bytes for an item id, loading and caching them on first
|
||||||
|
// access. An absent file caches an empty QByteArray so it is not retried.
|
||||||
|
const QByteArray& getSvg(const std::string& itemId);
|
||||||
|
|
||||||
|
QString m_iconDir;
|
||||||
|
std::map<std::string, QByteArray> m_svgById;
|
||||||
|
std::map<std::pair<std::string, int>, QPixmap> m_pixmapCache;
|
||||||
|
};
|
||||||
@@ -323,7 +323,11 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
|||||||
|
|
||||||
bool autoOpenLayout = false;
|
bool autoOpenLayout = false;
|
||||||
std::string chosenSchematic;
|
std::string chosenSchematic;
|
||||||
RecipeSelectionDialog dialog(options, title, this);
|
// Item icons live beside the config dir, mirroring the buildings icon path
|
||||||
|
// (REQ-UI-ITEM-ICON, REQ-UI-BUILD-ICON).
|
||||||
|
const QString itemIconDir = QDir::cleanPath(
|
||||||
|
QString::fromStdString(m_configDir) + "/../icons/items");
|
||||||
|
RecipeSelectionDialog dialog(options, title, itemIconDir, this);
|
||||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QSize>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@
|
|||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
#include "RecipeTooltip.h"
|
#include "RecipeTooltip.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
@@ -83,23 +87,38 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
|||||||
{
|
{
|
||||||
continue;
|
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);
|
||||||
options.push_back({recipe.id,
|
options.push_back({recipe.id,
|
||||||
QString::fromStdString(toDisplayName(recipe.id)),
|
QString::fromStdString(toDisplayName(recipe.id)),
|
||||||
buildRecipeTooltip(recipe)});
|
buildRecipeTooltip(recipe),
|
||||||
|
iconItemId});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// On-screen size of a recipe option button's item icon (REQ-UI-RECIPE-ICON).
|
||||||
|
const QSize kOptionIconSize(32, 32);
|
||||||
|
}
|
||||||
|
|
||||||
RecipeSelectionDialog::RecipeSelectionDialog(
|
RecipeSelectionDialog::RecipeSelectionDialog(
|
||||||
const std::vector<RecipeSelectionOption>& options,
|
const std::vector<RecipeSelectionOption>& options,
|
||||||
const QString& title, QWidget* parent)
|
const QString& title, const QString& itemIconDir, QWidget* parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
{
|
{
|
||||||
setWindowTitle(title);
|
setWindowTitle(title);
|
||||||
setModal(true);
|
setModal(true);
|
||||||
|
|
||||||
|
ItemIconCache iconCache(itemIconDir);
|
||||||
|
|
||||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||||
QGridLayout* grid = new QGridLayout();
|
QGridLayout* grid = new QGridLayout();
|
||||||
mainLayout->addLayout(grid);
|
mainLayout->addLayout(grid);
|
||||||
@@ -109,7 +128,19 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
|||||||
{
|
{
|
||||||
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
|
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
|
||||||
|
|
||||||
QPushButton* button = new QPushButton(option.caption, this);
|
QPushButton* button = new QPushButton(this);
|
||||||
|
// Icon-only when the produced item has an icon (REQ-UI-RECIPE-ICON); otherwise
|
||||||
|
// fall back to the caption. The name stays reachable via the tooltip.
|
||||||
|
if (!option.iconItemId.empty() && iconCache.hasIcon(option.iconItemId))
|
||||||
|
{
|
||||||
|
button->setIcon(QIcon(iconCache.getPixmap(
|
||||||
|
option.iconItemId, kOptionIconSize.width())));
|
||||||
|
button->setIconSize(kOptionIconSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button->setText(option.caption);
|
||||||
|
}
|
||||||
if (!option.tooltip.isEmpty())
|
if (!option.tooltip.isEmpty())
|
||||||
{
|
{
|
||||||
button->setToolTip(option.tooltip);
|
button->setToolTip(option.tooltip);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ struct RecipeSelectionOption
|
|||||||
std::string id;
|
std::string id;
|
||||||
QString caption;
|
QString caption;
|
||||||
QString tooltip;
|
QString tooltip;
|
||||||
|
// Id of the item whose icon is shown on the option button (REQ-UI-RECIPE-ICON);
|
||||||
|
// empty for the "(None)" option and for Shipyard schematics, which keep their
|
||||||
|
// caption. When set but no icon file exists, the button falls back to the caption.
|
||||||
|
std::string iconItemId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Builds the lock-aware list of selectable options for a production building
|
// Builds the lock-aware list of selectable options for a production building
|
||||||
@@ -39,8 +43,12 @@ class RecipeSelectionDialog : public QDialog
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// itemIconDir is the directory holding per-item icon SVGs (REQ-UI-ITEM-ICON);
|
||||||
|
// used to render recipe options icon-only (REQ-UI-RECIPE-ICON). Options whose
|
||||||
|
// item has no icon file fall back to their caption text.
|
||||||
RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options,
|
RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options,
|
||||||
const QString& title, QWidget* parent = nullptr);
|
const QString& title, const QString& itemIconDir,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
std::optional<std::string> getChosenId() const;
|
std::optional<std::string> getChosenId() const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user