show recipes visually instead of describing them in text
This commit is contained in:
@@ -25,7 +25,9 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
|
||||
@@ -59,7 +61,9 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp
|
||||
|
||||
@@ -114,7 +114,7 @@ QPixmap ItemIconCache::getPixmap(const std::string& cacheKey, const std::string&
|
||||
if (withSquare)
|
||||
{
|
||||
// Null unless the item has something to draw -- a square, an icon, or both --
|
||||
// so a caller with nothing to show can fall back to text (REQ-UI-RECIPE-ICON).
|
||||
// so a caller with nothing to show can fall back to text (REQ-UI-ITEM-ICON).
|
||||
const bool hasSquare = m_visuals != nullptr
|
||||
&& m_visuals->items.find(itemId) != m_visuals->items.end();
|
||||
if (hasSquare || hasIcon(itemId))
|
||||
|
||||
@@ -18,9 +18,11 @@ struct VisualsConfig;
|
||||
//
|
||||
// The square is the item's `fill` and `outline` from visuals.toml with the icon inset
|
||||
// within it, and it backs the icon both in the game world and wherever the UI displays
|
||||
// an item as an item: the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON),
|
||||
// the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the
|
||||
// recipe summary (REQ-UI-RECIPE-SUMMARY). Defined here once rather than per widget, in
|
||||
// an item as an item: the selection panel's item chips (REQ-UI-SINGLE-SELECTION,
|
||||
// REQ-UI-HQ-PANEL), and every recipe drawn as a line -- the recipe summary, the
|
||||
// selection dialog's option buttons, the item production tooltip
|
||||
// (REQ-UI-RECIPE-SUMMARY, REQ-UI-SELECT-OPTIONS, REQ-UI-ITEM-TOOLTIP). Defined here
|
||||
// once rather than per widget, in
|
||||
// two forms: paintItem() for the world's fractional geometry, getSquarePixmap() for
|
||||
// widgets that want a ready-made pixmap.
|
||||
//
|
||||
|
||||
60
src/ui/ItemProducers.cpp
Normal file
60
src/ui/ItemProducers.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "ItemProducers.h"
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool producesItem(const RecipeDef& recipe, const std::string& itemId)
|
||||
{
|
||||
for (const RecipeOutput& output : recipe.outputs)
|
||||
{
|
||||
if (output.item == itemId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isAvailable(const RecipeDef& recipe, const Simulation& sim)
|
||||
{
|
||||
if (recipe.building == BuildingType::Miner
|
||||
|| recipe.building == BuildingType::Assembler)
|
||||
{
|
||||
return sim.isRecipeUnlocked(recipe.id);
|
||||
}
|
||||
return sim.isBuildingUnlocked(recipe.building);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemProduction findItemProduction(const std::string& itemId, const Simulation& sim,
|
||||
const GameConfig& config)
|
||||
{
|
||||
ItemProduction result;
|
||||
|
||||
bool anyProducer = false;
|
||||
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||
{
|
||||
if (!producesItem(recipe, itemId)) { continue; }
|
||||
|
||||
anyProducer = true;
|
||||
if (isAvailable(recipe, sim))
|
||||
{
|
||||
result.recipes.push_back(&recipe);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.recipes.empty())
|
||||
{
|
||||
result.origin = ItemOrigin::Produced;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.origin = anyProducer ? ItemOrigin::Undiscovered : ItemOrigin::Salvaged;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
35
src/ui/ItemProducers.h
Normal file
35
src/ui/ItemProducers.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct GameConfig;
|
||||
struct RecipeDef;
|
||||
class Simulation;
|
||||
|
||||
// Where an item comes from, as far as the player has discovered (REQ-UI-ITEM-TOOLTIP).
|
||||
enum class ItemOrigin
|
||||
{
|
||||
Produced, // at least one recipe the player can run makes it
|
||||
Undiscovered, // recipes make it, but none of them is available yet
|
||||
Salvaged, // no recipe makes it at all -- scrap, which comes from debris
|
||||
};
|
||||
|
||||
struct ItemProduction
|
||||
{
|
||||
ItemOrigin origin = ItemOrigin::Salvaged;
|
||||
// The available producers, in config order. Empty unless origin is Produced.
|
||||
std::vector<const RecipeDef*> recipes;
|
||||
};
|
||||
|
||||
// Every way the player can currently produce the given item, for the tooltip that
|
||||
// explains an item chip (REQ-UI-ITEM-TOOLTIP).
|
||||
//
|
||||
// What counts as available differs by building type, because only Miner and Assembler
|
||||
// recipes are individually unlocked (REQ-LOCK-UI-RECIPE): those are filtered by the
|
||||
// unlock state, while a Smelter's or Reprocessing Plant's implicit recipes
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) are filtered by whether their building is
|
||||
// unlocked yet (REQ-LOCK-BUILDING) -- there is no sense in naming a path through a
|
||||
// plant the player cannot place.
|
||||
ItemProduction findItemProduction(const std::string& itemId, const Simulation& sim,
|
||||
const GameConfig& config);
|
||||
@@ -224,7 +224,8 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this);
|
||||
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes,
|
||||
m_itemIcons.get(), m_buildingIcons.get(), this);
|
||||
dialog.exec();
|
||||
|
||||
std::shared_ptr<ApplySchematicChoiceCommand> command =
|
||||
@@ -317,7 +318,7 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||
m_layoutBlueprints,
|
||||
std::move(unlockedModuleIds),
|
||||
m_gameWorldView->isDebugDrawEnabled(),
|
||||
this);
|
||||
m_itemIcons.get(), this);
|
||||
// Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after
|
||||
// a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way.
|
||||
placeOnSelectionPanel(dialog);
|
||||
@@ -427,7 +428,8 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
|
||||
bool autoOpenLayout = false;
|
||||
std::string chosenSchematic;
|
||||
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this);
|
||||
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(),
|
||||
m_buildingIcons.get(), this);
|
||||
placeOnSelectionPanel(dialog);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||
{
|
||||
|
||||
35
src/ui/OptionButton.cpp
Normal file
35
src/ui/OptionButton.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "OptionButton.h"
|
||||
|
||||
#include <QLayout>
|
||||
#include <QStyle>
|
||||
#include <QStyleOptionButton>
|
||||
|
||||
OptionButton::OptionButton(QWidget* parent)
|
||||
: QPushButton(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QSize OptionButton::sizeHint() const
|
||||
{
|
||||
if (layout() == nullptr)
|
||||
{
|
||||
return QPushButton::sizeHint();
|
||||
}
|
||||
return sizeForFace(layout()->sizeHint());
|
||||
}
|
||||
|
||||
QSize OptionButton::minimumSizeHint() const
|
||||
{
|
||||
if (layout() == nullptr)
|
||||
{
|
||||
return QPushButton::minimumSizeHint();
|
||||
}
|
||||
return sizeForFace(layout()->minimumSize());
|
||||
}
|
||||
|
||||
QSize OptionButton::sizeForFace(const QSize& faceSize) const
|
||||
{
|
||||
QStyleOptionButton option;
|
||||
initStyleOption(&option);
|
||||
return style()->sizeFromContents(QStyle::CT_PushButton, &option, faceSize, this);
|
||||
}
|
||||
28
src/ui/OptionButton.h
Normal file
28
src/ui/OptionButton.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QSize>
|
||||
|
||||
// A push button whose face is built of widgets rather than of a caption: an option's
|
||||
// name over the recipe line stating what it makes (REQ-UI-SELECT-OPTIONS), or a module's
|
||||
// name over what it costs (REQ-MOD-UI-DIALOG).
|
||||
//
|
||||
// It exists only to be measured correctly. QPushButton computes its size hint from its
|
||||
// text and icon and ignores a layout set on it, so a button carrying a face of widgets
|
||||
// would be sized as if it were empty and clip everything in it. This asks the layout
|
||||
// instead, and then lets the style add what the button's own frame needs.
|
||||
class OptionButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OptionButton(QWidget* parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
private:
|
||||
// The style's button size for the given face size, or the plain QPushButton hint
|
||||
// when there is no face to measure.
|
||||
QSize sizeForFace(const QSize& faceSize) const;
|
||||
};
|
||||
140
src/ui/RecipeLineRow.cpp
Normal file
140
src/ui/RecipeLineRow.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "RecipeLineRow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
#include <QPixmap>
|
||||
|
||||
#include "BuildingIconCache.h"
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the items' colored squares and the building's chip are drawn at on a line, in
|
||||
// device-independent pixels. Larger than the artwork they carry, which the square
|
||||
// insets (REQ-UI-ITEM-ICON).
|
||||
const int kIconSizePx = 18;
|
||||
|
||||
// Adds a freshly built label to the line and shows it.
|
||||
//
|
||||
// The show is what makes it count: a widget created under an already-visible parent
|
||||
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
|
||||
// add nothing to the size hint the surrounding widget measures itself against as soon
|
||||
// as this returns.
|
||||
void addAndShow(QHBoxLayout* layout, QLabel* label)
|
||||
{
|
||||
layout->addWidget(label);
|
||||
label->show();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
RecipeLineRow::RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
|
||||
QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_buildingIcons(buildingIcons)
|
||||
{
|
||||
m_layout = new QHBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(4);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void RecipeLineRow::setLine(const Spec& spec)
|
||||
{
|
||||
if (spec.isEmpty())
|
||||
{
|
||||
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
|
||||
// a change and shows the row again.
|
||||
m_spec = Spec();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
if (spec == m_spec)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_spec = spec;
|
||||
rebuild(spec);
|
||||
show();
|
||||
}
|
||||
|
||||
void RecipeLineRow::rebuild(const Spec& spec)
|
||||
{
|
||||
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
// The building the recipe runs in, where the line has to distinguish one producer
|
||||
// from another (REQ-UI-ITEM-TOOLTIP). A building with no chip file leaves the icon
|
||||
// off, as everywhere else (REQ-UI-BUILD-ICON).
|
||||
if (spec.building.has_value() && m_buildingIcons != nullptr)
|
||||
{
|
||||
const QPixmap chip =
|
||||
m_buildingIcons->getChip(buildingTypeId(*spec.building), kIconSizePx);
|
||||
if (!chip.isNull())
|
||||
{
|
||||
QLabel* chipLabel = new QLabel(this);
|
||||
chipLabel->setPixmap(chip);
|
||||
addAndShow(m_layout, chipLabel);
|
||||
}
|
||||
}
|
||||
if (!spec.name.isEmpty())
|
||||
{
|
||||
addAndShow(m_layout, new QLabel(spec.name, this));
|
||||
}
|
||||
|
||||
addAmounts(spec.inputs);
|
||||
if (!spec.inputs.empty() && !spec.outputs.empty())
|
||||
{
|
||||
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||
addAndShow(m_layout, new QLabel(QString(rightArrow), this));
|
||||
}
|
||||
addAmounts(spec.outputs);
|
||||
|
||||
if (spec.durationSeconds.has_value() && *spec.durationSeconds > 0.0)
|
||||
{
|
||||
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
||||
const QString time = spec.durationIsAddition
|
||||
? tr("+%1 s").arg(*spec.durationSeconds, 0, 'f', 1)
|
||||
: tr("%1 s").arg(*spec.durationSeconds, 0, 'f', 1);
|
||||
addAndShow(m_layout, new QLabel(
|
||||
QStringLiteral("%1 %2").arg(middleDot).arg(time), this));
|
||||
}
|
||||
m_layout->addStretch(1);
|
||||
}
|
||||
|
||||
void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts)
|
||||
{
|
||||
for (const Amount& entry : amounts)
|
||||
{
|
||||
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
||||
// is not an error: the square stands alone then, and only an item with no square
|
||||
// either falls back to its id in text.
|
||||
const QPixmap icon = (m_itemIcons != nullptr)
|
||||
? m_itemIcons->getSquarePixmap(entry.itemId, kIconSizePx)
|
||||
: QPixmap();
|
||||
if (!icon.isNull())
|
||||
{
|
||||
QLabel* iconLabel = new QLabel(this);
|
||||
iconLabel->setPixmap(icon);
|
||||
addAndShow(m_layout, iconLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
addAndShow(m_layout,
|
||||
new QLabel(QString::fromStdString(entry.itemId), this));
|
||||
}
|
||||
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this));
|
||||
}
|
||||
}
|
||||
98
src/ui/RecipeLineRow.h
Normal file
98
src/ui/RecipeLineRow.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "BuildingType.h"
|
||||
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
class QHBoxLayout;
|
||||
|
||||
// One recipe drawn on a line: what it consumes, an arrow, what it produces, and how
|
||||
// long a cycle takes (REQ-UI-RECIPE-SUMMARY). Every place the UI states what something
|
||||
// makes draws this same line, so a recipe reads alike wherever it is shown:
|
||||
//
|
||||
// * beneath the selection button, for the recipe a building is running
|
||||
// (REQ-UI-RECIPE-SUMMARY);
|
||||
// * on a selection dialog's option buttons, under the option's name
|
||||
// (REQ-UI-SELECT-OPTIONS);
|
||||
// * in an item's production tooltip and in the unlock-choice dialog, there led by the
|
||||
// icon of the building that runs the recipe and its name
|
||||
// (REQ-UI-ITEM-TOOLTIP, REQ-DEF-SCHEMATIC-DROP);
|
||||
// * on a module button and in the layout dialog's build cost, where there is a price
|
||||
// and a time but nothing produced (REQ-MOD-UI-DIALOG, REQ-MOD-UI-STATS-PANEL).
|
||||
class RecipeLineRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// One item and how many of it the line states.
|
||||
struct Amount
|
||||
{
|
||||
std::string itemId;
|
||||
int amount = 0;
|
||||
|
||||
bool operator==(const Amount& other) const
|
||||
{
|
||||
return itemId == other.itemId && amount == other.amount;
|
||||
}
|
||||
};
|
||||
|
||||
// What the line says. Everything but the inputs is optional, which is what lets the
|
||||
// one widget serve the summary, the option buttons, the tooltip lines and the cost
|
||||
// lines.
|
||||
struct Spec
|
||||
{
|
||||
// Drawn as a leading chip when the line has to say which building runs the
|
||||
// recipe -- an item may have several producers (REQ-UI-ITEM-TOOLTIP). Unset
|
||||
// wherever the surrounding widget already establishes the building.
|
||||
std::optional<BuildingType> building;
|
||||
// Leading recipe name, for the same reason. Empty where the name is the
|
||||
// caption above the line instead.
|
||||
QString name;
|
||||
std::vector<Amount> inputs;
|
||||
// Empty for a line that produces no item of its own: a ship schematic, or a
|
||||
// module's price. No arrow is drawn then.
|
||||
std::vector<Amount> outputs;
|
||||
std::optional<double> durationSeconds;
|
||||
// True where the time is added to something else rather than being a cycle of
|
||||
// its own, and so reads "+3.0 s" (REQ-MOD-UI-DIALOG).
|
||||
bool durationIsAddition = false;
|
||||
|
||||
bool operator==(const Spec& other) const
|
||||
{
|
||||
return building == other.building && name == other.name
|
||||
&& inputs == other.inputs && outputs == other.outputs
|
||||
&& durationSeconds == other.durationSeconds
|
||||
&& durationIsAddition == other.durationIsAddition;
|
||||
}
|
||||
|
||||
bool isEmpty() const { return inputs.empty() && outputs.empty(); }
|
||||
};
|
||||
|
||||
// Both caches may be null, which leaves the icons off: an item with no square and
|
||||
// no icon falls back to its id, and a building with no chip to its name alone.
|
||||
// Neither is owned.
|
||||
RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
// Hides the row when the spec has nothing to state, which is how a building with no
|
||||
// recipe shows no summary at all (REQ-UI-RECIPE-SUMMARY).
|
||||
void setLine(const Spec& spec);
|
||||
|
||||
private:
|
||||
void rebuild(const Spec& spec);
|
||||
void addAmounts(const std::vector<Amount>& amounts);
|
||||
|
||||
ItemIconCache* m_itemIcons;
|
||||
BuildingIconCache* m_buildingIcons;
|
||||
QHBoxLayout* m_layout;
|
||||
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
|
||||
// recipe actually changed.
|
||||
Spec m_spec;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
#include <QString>
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "RecipeLineRow.h"
|
||||
|
||||
struct GameConfig;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
class Simulation;
|
||||
class QPushButton;
|
||||
class ItemIconCache;
|
||||
|
||||
// One selectable entry in the recipe/schematic selection dialog
|
||||
// (REQ-UI-SELECT-BUTTON). The "(None)" entry uses an empty id.
|
||||
@@ -20,23 +22,23 @@ struct RecipeSelectionOption
|
||||
{
|
||||
std::string id;
|
||||
QString caption;
|
||||
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;
|
||||
// What the option makes, drawn beneath its caption on the button
|
||||
// (REQ-UI-SELECT-OPTIONS). Empty for the "(None)" option, which is a caption alone.
|
||||
// A ship schematic fills the inputs and the time and leaves the outputs empty: a
|
||||
// ship is not an item and has no icon, so the caption is what it produces.
|
||||
RecipeLineRow::Spec line;
|
||||
};
|
||||
|
||||
// Builds the lock-aware list of selectable options for a production building
|
||||
// of the given type, with display captions and info tooltips
|
||||
// (REQ-UI-SELECT-BUTTON, REQ-UI-SELECT-TOOLTIP). The first entry is always a
|
||||
// of the given type, with display captions and recipe lines
|
||||
// (REQ-UI-SELECT-BUTTON, REQ-UI-SELECT-OPTIONS). The first entry is always a
|
||||
// "(None)" option with an empty id. Shared by the selection dialog and the
|
||||
// panel selection button so both render identical captions and tooltips, and
|
||||
// panel selection button so both render identical captions, and
|
||||
// usable for construction sites which have no Building yet (REQ-BLD-SITE-CONFIG).
|
||||
std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
||||
BuildingType type, Simulation& sim, const GameConfig& config);
|
||||
|
||||
// Modal dialog showing a grid of option buttons (REQ-UI-SELECT-BUTTON). The
|
||||
// Modal dialog listing the options in one vertical column (REQ-UI-SELECT-OPTIONS). The
|
||||
// game is paused by the caller while it is open. Clicking an option selects it
|
||||
// and closes the dialog; dismissing it (close/Esc) leaves no choice.
|
||||
class RecipeSelectionDialog : public QDialog
|
||||
@@ -44,12 +46,10 @@ class RecipeSelectionDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// itemIcons is the window-wide per-item icon cache (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. Not owned.
|
||||
// The icon caches draw each option's recipe line (REQ-UI-ITEM-ICON). Not owned.
|
||||
RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options,
|
||||
const QString& title, ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||
|
||||
std::optional<std::string> getChosenId() const;
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#include "RecipeTooltip.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include "DisplayName.h"
|
||||
#include "RecipesConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QString itemLine(const std::string& itemId, int amount)
|
||||
{
|
||||
return QStringLiteral(" ")
|
||||
+ QString::fromStdString(toDisplayName(itemId))
|
||||
+ QStringLiteral(" x ") + QString::number(amount);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString buildRecipeTooltip(const RecipeDef& recipe)
|
||||
{
|
||||
QStringList lines;
|
||||
lines << QObject::tr("Recipe: %1")
|
||||
.arg(QString::fromStdString(toDisplayName(recipe.id)));
|
||||
|
||||
if (recipe.inputs.empty())
|
||||
{
|
||||
lines << QObject::tr("Inputs: none");
|
||||
}
|
||||
else
|
||||
{
|
||||
lines << QObject::tr("Inputs:");
|
||||
for (const RecipeIngredient& ingredient : recipe.inputs)
|
||||
{
|
||||
lines << itemLine(ingredient.item, ingredient.amount);
|
||||
}
|
||||
}
|
||||
|
||||
lines << QObject::tr("Completion time: %1 s").arg(recipe.durationSeconds);
|
||||
|
||||
if (!recipe.outputs.empty())
|
||||
{
|
||||
lines << QObject::tr("Produces:");
|
||||
for (const RecipeOutput& output : recipe.outputs)
|
||||
{
|
||||
lines << itemLine(output.item, output.amount);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct RecipeDef;
|
||||
|
||||
// Builds the recipe info tooltip text (REQ-UI-SELECT-TOOLTIP): the recipe name,
|
||||
// each input item name and quantity, the completion time, and the produced
|
||||
// output item name and quantity. Shared by the recipe-selection dialog and the
|
||||
// schematic choice dialog's "Unlocks recipes:" list so both render identically.
|
||||
QString buildRecipeTooltip(const RecipeDef& recipe);
|
||||
@@ -6,12 +6,35 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "DisplayName.h"
|
||||
#include "RecipeTooltip.h"
|
||||
#include "RecipeLineRow.h"
|
||||
#include "RecipesConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(
|
||||
const std::vector<RecipeIngredient>& ingredients)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
|
||||
{
|
||||
std::vector<RecipeLineRow::Amount> amounts;
|
||||
amounts.reserve(outputs.size());
|
||||
for (const RecipeOutput& output : outputs)
|
||||
{
|
||||
amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
QString grantKindLabel(SchematicType type)
|
||||
{
|
||||
switch (type)
|
||||
@@ -28,7 +51,8 @@ QString grantKindLabel(SchematicType type)
|
||||
|
||||
SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
const std::vector<SchematicChoiceOption>& options,
|
||||
const RecipesConfig& recipes,
|
||||
const RecipesConfig& recipes, ItemIconCache* itemIcons,
|
||||
BuildingIconCache* buildingIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_chosenIndex(0)
|
||||
@@ -108,14 +132,23 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
{
|
||||
for (const std::string& recipeId : option.newlyUnlockedRecipeIds)
|
||||
{
|
||||
QLabel* recipeLabel = new QLabel(
|
||||
QString::fromStdString(toDisplayName(recipeId)), card);
|
||||
recipeLabel->setAlignment(Qt::AlignCenter);
|
||||
if (const RecipeDef* def = recipes.findRecipeDef(recipeId))
|
||||
{
|
||||
recipeLabel->setToolTip(buildRecipeTooltip(*def));
|
||||
}
|
||||
cardLayout->addWidget(recipeLabel);
|
||||
const RecipeDef* def = recipes.findRecipeDef(recipeId);
|
||||
if (def == nullptr) { continue; }
|
||||
|
||||
// The recipe drawn as it is everywhere else, led by the building
|
||||
// that runs it and its name, so the line says everything there is
|
||||
// to say about it and carries no tooltip (REQ-DEF-SCHEMATIC-DROP).
|
||||
RecipeLineRow::Spec spec;
|
||||
spec.building = def->building;
|
||||
spec.name = QString::fromStdString(toDisplayName(def->id));
|
||||
spec.inputs = toAmounts(def->inputs);
|
||||
spec.outputs = toAmounts(def->outputs);
|
||||
spec.durationSeconds = def->durationSeconds;
|
||||
|
||||
RecipeLineRow* line =
|
||||
new RecipeLineRow(itemIcons, buildingIcons, card);
|
||||
cardLayout->addWidget(line);
|
||||
line->setLine(spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,19 @@
|
||||
#include "SchematicChoiceOption.h"
|
||||
|
||||
struct RecipesConfig;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
|
||||
class SchematicChoiceDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// The icon caches draw the recipe lines of the "Unlocks recipes" list
|
||||
// (REQ-DEF-SCHEMATIC-DROP, REQ-UI-ITEM-ICON, REQ-UI-BUILD-ICON). Not owned.
|
||||
SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options,
|
||||
const RecipesConfig& recipes,
|
||||
const RecipesConfig& recipes, ItemIconCache* itemIcons,
|
||||
BuildingIconCache* buildingIcons,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
int getChosenIndex() const;
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
#include <functional>
|
||||
|
||||
#include "DisplayName.h"
|
||||
#include "OptionButton.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "RecipeLineRow.h"
|
||||
#include "SectionBox.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
@@ -22,6 +27,18 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(
|
||||
const std::vector<RecipeIngredient>& ingredients)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
const int kCellSize = 32;
|
||||
|
||||
// Empty ship tiles pulse between this grey and white at 1 Hz while a module is
|
||||
@@ -401,9 +418,11 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||
std::set<std::string> unlockedModuleIds,
|
||||
bool debugDraw,
|
||||
ItemIconCache* itemIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_config(config)
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_shipId(shipId)
|
||||
, m_unlockedModuleIds(std::move(unlockedModuleIds))
|
||||
, m_rows(0)
|
||||
@@ -463,10 +482,22 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
// Middle: three-column area (stats | module buttons | blueprints).
|
||||
QHBoxLayout* columnsLayout = new QHBoxLayout();
|
||||
|
||||
// Left column: ship stats panel.
|
||||
// Left column: ship stats panel, and beneath it what the layout being configured
|
||||
// would cost to build (REQ-MOD-UI-DIALOG). The cost sits outside the stats panel
|
||||
// because that widget is shared with the live ship card and the balancing tool,
|
||||
// neither of which costs anything -- and the balancing tool is deliberately built
|
||||
// without the item icons this line draws with.
|
||||
QVBoxLayout* leftLayout = new QVBoxLayout();
|
||||
m_statsPanel = new ShipStatsPanel(config, this);
|
||||
m_statsPanel->setDebugDrawEnabled(m_debugDraw);
|
||||
columnsLayout->addWidget(m_statsPanel);
|
||||
leftLayout->addWidget(m_statsPanel);
|
||||
|
||||
m_buildCostSection = new SectionBox(tr("Build cost"), this);
|
||||
m_buildCostLine = new RecipeLineRow(m_itemIcons, nullptr, m_buildCostSection);
|
||||
m_buildCostSection->getContentLayout()->addWidget(m_buildCostLine);
|
||||
leftLayout->addWidget(m_buildCostSection);
|
||||
leftLayout->addStretch(1);
|
||||
columnsLayout->addLayout(leftLayout);
|
||||
|
||||
// Center column: module selection buttons.
|
||||
QVBoxLayout* centerLayout = new QVBoxLayout();
|
||||
@@ -487,11 +518,34 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
m_moduleButtons.push_back(nullptr);
|
||||
continue;
|
||||
}
|
||||
const QString label = QString::fromStdString(toDisplayName(def.id))
|
||||
+ "\n" + QString::fromStdString(def.glyph);
|
||||
QPushButton* btn = new QPushButton(label, this);
|
||||
// The module's name over what it costs -- its materials and the production time
|
||||
// it adds (REQ-MOD-UI-DIALOG). The glyph is how a module is identified on the
|
||||
// layout grid, not here. Both children are transparent to the mouse so a click
|
||||
// anywhere on the face still reaches the button.
|
||||
OptionButton* btn = new OptionButton(this);
|
||||
btn->setCheckable(true);
|
||||
btn->setFixedHeight(48);
|
||||
|
||||
QVBoxLayout* face = new QVBoxLayout(btn);
|
||||
face->setContentsMargins(8, 4, 8, 4);
|
||||
face->setSpacing(1);
|
||||
|
||||
QLabel* nameLabel =
|
||||
new QLabel(QString::fromStdString(toDisplayName(def.id)), btn);
|
||||
nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
face->addWidget(nameLabel);
|
||||
|
||||
RecipeLineRow::Spec cost;
|
||||
cost.inputs = toAmounts(def.materials);
|
||||
cost.durationSeconds = def.productionTimeSeconds;
|
||||
cost.durationIsAddition = true;
|
||||
|
||||
RecipeLineRow* costLine = new RecipeLineRow(m_itemIcons, nullptr, btn);
|
||||
costLine->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
face->addWidget(costLine);
|
||||
costLine->setLine(cost);
|
||||
|
||||
// The config tooltip stays: it says what the module does, which the cost line
|
||||
// does not (REQ-MOD-UI-MODULE-TOOLTIP).
|
||||
if (def.tooltip)
|
||||
{
|
||||
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
||||
@@ -731,6 +785,29 @@ void ShipLayoutDialog::updateGridWidget()
|
||||
void ShipLayoutDialog::updateStats()
|
||||
{
|
||||
m_statsPanel->refresh(m_shipId, m_placedModules);
|
||||
|
||||
// What the layout as it currently stands would cost to build, so the price follows
|
||||
// every placement and removal (REQ-MOD-UI-DIALOG). The sums come from the
|
||||
// simulation's own rules rather than being added up a second time here
|
||||
// (REQ-MOD-MATERIALS, REQ-MOD-PRODUCTION-TIME).
|
||||
ShipLayoutConfig layout;
|
||||
layout.placedModules = m_placedModules;
|
||||
const std::map<std::string, int> materials =
|
||||
computeShipyardRequiredMaterials(*m_config, m_shipId, layout);
|
||||
|
||||
RecipeLineRow::Spec cost;
|
||||
cost.inputs.reserve(materials.size());
|
||||
for (const std::pair<const std::string, int>& entry : materials)
|
||||
{
|
||||
cost.inputs.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
|
||||
}
|
||||
cost.durationSeconds =
|
||||
computeShipyardProductionTimeSeconds(*m_config, m_shipId, layout);
|
||||
|
||||
m_buildCostLine->setLine(cost);
|
||||
// A schematic costing no materials at all has no line to draw, and the section then
|
||||
// stays out of the way.
|
||||
m_buildCostSection->setVisible(!cost.inputs.empty());
|
||||
}
|
||||
|
||||
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipLayoutBlueprint.h"
|
||||
|
||||
class ItemIconCache;
|
||||
class QPushButton;
|
||||
class RecipeLineRow;
|
||||
class SectionBox;
|
||||
class ShipStatsPanel;
|
||||
|
||||
class ShipLayoutDialog : public QDialog
|
||||
@@ -22,12 +25,15 @@ class ShipLayoutDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// itemIcons draws what each module costs and what the layout costs in total
|
||||
// (REQ-MOD-UI-DIALOG, REQ-MOD-UI-STATS-PANEL). Not owned.
|
||||
ShipLayoutDialog(const GameConfig* config,
|
||||
const std::string& shipId,
|
||||
const ShipLayoutConfig& currentLayout,
|
||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||
std::set<std::string> unlockedModuleIds,
|
||||
bool debugDraw,
|
||||
ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
std::optional<ShipLayoutConfig> getResult() const;
|
||||
@@ -60,6 +66,7 @@ private:
|
||||
void loadLayoutBlueprint(const std::vector<PlacedModule>& modules);
|
||||
|
||||
const GameConfig* m_config;
|
||||
ItemIconCache* m_itemIcons;
|
||||
std::string m_shipId;
|
||||
std::set<std::string> m_unlockedModuleIds;
|
||||
std::vector<std::string> m_shipLayout;
|
||||
@@ -81,6 +88,8 @@ private:
|
||||
QPushButton* m_removeButton;
|
||||
QWidget* m_gridWidget;
|
||||
ShipStatsPanel* m_statsPanel;
|
||||
SectionBox* m_buildCostSection;
|
||||
RecipeLineRow* m_buildCostLine;
|
||||
bool m_debugDraw;
|
||||
|
||||
std::optional<ShipLayoutConfig> m_result;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "DisplayName.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "ProductionSection.h"
|
||||
#include "RecipeSummaryRow.h"
|
||||
#include "RecipeLineRow.h"
|
||||
#include "SectionBox.h"
|
||||
#include "SelectionNames.h"
|
||||
#include "Simulation.h"
|
||||
@@ -18,13 +18,13 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
||||
{
|
||||
std::vector<RecipeSummaryRow::Amount> amounts;
|
||||
std::vector<RecipeLineRow::Amount> amounts;
|
||||
amounts.reserve(map.size());
|
||||
for (const std::pair<const std::string, int>& entry : map)
|
||||
{
|
||||
amounts.push_back(RecipeSummaryRow::Amount{ entry.first, entry.second });
|
||||
amounts.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
@@ -66,17 +66,17 @@ BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context
|
||||
{
|
||||
// The summary is configuration -- what the building will do -- so it sits with the
|
||||
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
|
||||
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
|
||||
m_recipeSummary = new RecipeLineRow(context.itemIcons, context.buildingIcons, this);
|
||||
getConfigurationLayout()->addWidget(m_recipeSummary);
|
||||
|
||||
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
||||
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
|
||||
m_inputChips = new ItemChipRow(context, m_inputSection);
|
||||
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
||||
|
||||
m_production = new ProductionSection(this);
|
||||
|
||||
m_outputSection = new SectionBox(tr("Output buffer"), this);
|
||||
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
|
||||
m_outputChips = new ItemChipRow(context, m_outputSection);
|
||||
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
||||
|
||||
// In the direction the materials flow: what goes in, what is being made of it, what
|
||||
@@ -98,10 +98,14 @@ void BufferedBuildingContent::refreshConfiguration()
|
||||
|
||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||
|
||||
// The card already names the building in its header, so the summary states the cycle
|
||||
// alone -- no building chip, no recipe name (REQ-UI-RECIPE-SUMMARY).
|
||||
const CycleInfo cycle = getCycleInfo(target);
|
||||
m_recipeSummary->setSummary(toAmounts(cycle.perCycleInputs),
|
||||
toAmounts(cycle.perCycleOutputs),
|
||||
cycle.durationSeconds);
|
||||
RecipeLineRow::Spec summary;
|
||||
summary.inputs = toAmounts(cycle.perCycleInputs);
|
||||
summary.outputs = toAmounts(cycle.perCycleOutputs);
|
||||
summary.durationSeconds = cycle.durationSeconds;
|
||||
m_recipeSummary->setLine(summary);
|
||||
|
||||
refreshControls(target);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
struct Building;
|
||||
struct BuildingTarget;
|
||||
class ProductionSection;
|
||||
class RecipeSummaryRow;
|
||||
class RecipeLineRow;
|
||||
class SectionBox;
|
||||
|
||||
// Shared body of the four cards that show one building with buffers -- the Miner and
|
||||
@@ -74,7 +74,7 @@ private:
|
||||
|
||||
BuildingId m_id;
|
||||
|
||||
RecipeSummaryRow* m_recipeSummary;
|
||||
RecipeLineRow* m_recipeSummary;
|
||||
|
||||
// Input buffers, production progress, output buffer -- in that order, so the card
|
||||
// reads the way the materials flow (REQ-UI-SINGLE-SELECTION).
|
||||
|
||||
@@ -14,7 +14,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
|
||||
@@ -49,7 +49,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp
|
||||
|
||||
@@ -23,7 +23,7 @@ HqContent::HqContent(const SelectionContext& context, const SelectionRequest& re
|
||||
m_hpBar = new BarRow(tr("HP"), this);
|
||||
|
||||
m_stockSection = new SectionBox(tr("Building blocks"), this);
|
||||
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
|
||||
m_stockChips = new ItemChipRow(context, m_stockSection);
|
||||
m_stockSection->getContentLayout()->addWidget(m_stockChips);
|
||||
|
||||
// HP first, as on every card that has it (REQ-UI-SELECTION-CARD, REQ-UI-HQ-PANEL).
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "ItemChip.h"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ItemTooltip.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -13,11 +17,18 @@ namespace
|
||||
const int kCountSizeRisePt = 2;
|
||||
const int kSubLineSizeDropPt = 1;
|
||||
|
||||
// How long the cursor rests on a chip before its tooltip appears, in milliseconds.
|
||||
// Matches the feel of an ordinary tooltip, which this one replaces (REQ-UI-ITEM-TOOLTIP).
|
||||
const int kHoverDelayMs = 500;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
ItemChip::ItemChip(const QPixmap& icon, QWidget* parent)
|
||||
ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId,
|
||||
const QPixmap& icon, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_context(context)
|
||||
, m_itemId(itemId)
|
||||
{
|
||||
// Its own boxed chrome, drawn with palette colors like the rest of the panel's
|
||||
// furniture rather than from visuals.toml, which is for world rendering.
|
||||
@@ -58,6 +69,11 @@ ItemChip::ItemChip(const QPixmap& icon, QWidget* parent)
|
||||
|
||||
layout->addWidget(m_iconLabel);
|
||||
layout->addWidget(text);
|
||||
|
||||
m_hoverTimer = new QTimer(this);
|
||||
m_hoverTimer->setSingleShot(true);
|
||||
m_hoverTimer->setInterval(kHoverDelayMs);
|
||||
connect(m_hoverTimer, &QTimer::timeout, this, &ItemChip::showTooltip);
|
||||
}
|
||||
|
||||
void ItemChip::setCount(const QString& count)
|
||||
@@ -70,3 +86,28 @@ void ItemChip::setSubLine(const QString& subLine)
|
||||
m_subLineLabel->setText(subLine);
|
||||
m_subLineLabel->setVisible(!subLine.isEmpty());
|
||||
}
|
||||
|
||||
void ItemChip::enterEvent(QEvent* event)
|
||||
{
|
||||
m_hoverTimer->start();
|
||||
QWidget::enterEvent(event);
|
||||
}
|
||||
|
||||
void ItemChip::leaveEvent(QEvent* event)
|
||||
{
|
||||
m_hoverTimer->stop();
|
||||
if (m_tooltip != nullptr)
|
||||
{
|
||||
m_tooltip->hide();
|
||||
}
|
||||
QWidget::leaveEvent(event);
|
||||
}
|
||||
|
||||
void ItemChip::showTooltip()
|
||||
{
|
||||
if (m_tooltip == nullptr)
|
||||
{
|
||||
m_tooltip = new ItemTooltip(m_context, m_itemId, this);
|
||||
}
|
||||
m_tooltip->showAt(QCursor::pos());
|
||||
}
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "SelectionContext.h"
|
||||
|
||||
class ItemTooltip;
|
||||
class QEvent;
|
||||
class QLabel;
|
||||
class QTimer;
|
||||
|
||||
// One buffered item: its icon, its count in a larger type, and a sub-line beneath the
|
||||
// count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities
|
||||
// rather than as a run of text.
|
||||
//
|
||||
// Hovering it explains where its item comes from (REQ-UI-ITEM-TOOLTIP).
|
||||
class ItemChip : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// An empty icon leaves the icon off and the chip laid out around the text alone.
|
||||
ItemChip(const QPixmap& icon, QWidget* parent = nullptr);
|
||||
ItemChip(const SelectionContext& context, const std::string& itemId,
|
||||
const QPixmap& icon, QWidget* parent = nullptr);
|
||||
|
||||
void setCount(const QString& count);
|
||||
// Left empty for an item with nothing to say beneath its count.
|
||||
void setSubLine(const QString& subLine);
|
||||
|
||||
protected:
|
||||
void enterEvent(QEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
private:
|
||||
QLabel* m_iconLabel;
|
||||
QLabel* m_countLabel;
|
||||
QLabel* m_subLineLabel;
|
||||
void showTooltip();
|
||||
|
||||
SelectionContext m_context;
|
||||
std::string m_itemId;
|
||||
QLabel* m_iconLabel;
|
||||
QLabel* m_countLabel;
|
||||
QLabel* m_subLineLabel;
|
||||
// Held back so a cursor crossing the chip on its way elsewhere does not flash the
|
||||
// tooltip, the way an ordinary tooltip is.
|
||||
QTimer* m_hoverTimer;
|
||||
// Built on the first hover, because most chips are never hovered at all.
|
||||
ItemTooltip* m_tooltip = nullptr;
|
||||
};
|
||||
|
||||
@@ -19,9 +19,9 @@ const int kChipsPerRow = 2;
|
||||
} // namespace
|
||||
|
||||
|
||||
ItemChipRow::ItemChipRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||
ItemChipRow::ItemChipRow(const SelectionContext& context, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_context(context)
|
||||
{
|
||||
m_layout = new QGridLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -68,9 +68,10 @@ void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
|
||||
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
||||
// is not an error: the chip then carries the square alone, and only an item with
|
||||
// no square either leaves the chip laid out around its count alone.
|
||||
const QPixmap icon = m_itemIcons->getSquarePixmap(itemId, kChipIconSizePx);
|
||||
const QPixmap icon =
|
||||
m_context.itemIcons->getSquarePixmap(itemId, kChipIconSizePx);
|
||||
|
||||
ItemChip* chip = new ItemChip(icon, this);
|
||||
ItemChip* chip = new ItemChip(m_context, itemId, icon, this);
|
||||
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
||||
static_cast<int>(index) % kChipsPerRow);
|
||||
// Shown right away, because the panel measures itself as soon as this returns. A
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "SelectionContext.h"
|
||||
|
||||
class ItemChip;
|
||||
class ItemIconCache;
|
||||
class QGridLayout;
|
||||
|
||||
// The buffered items of one building, each as a chip carrying the item's icon, its
|
||||
@@ -28,16 +29,18 @@ public:
|
||||
QString subLine;
|
||||
};
|
||||
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||
// no icon file simply shows no icon, which is not an error. Not owned.
|
||||
explicit ItemChipRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||
// The context supplies the icon cache each chip draws its item with
|
||||
// (REQ-UI-ITEM-ICON) -- an item with no icon file simply shows no icon, which is not
|
||||
// an error -- and what a chip's tooltip reads to explain its item
|
||||
// (REQ-UI-ITEM-TOOLTIP).
|
||||
explicit ItemChipRow(const SelectionContext& context, QWidget* parent = nullptr);
|
||||
|
||||
void setEntries(const std::vector<Entry>& entries);
|
||||
|
||||
private:
|
||||
void rebuildChips(const std::vector<Entry>& entries);
|
||||
|
||||
ItemIconCache* m_itemIcons;
|
||||
SelectionContext m_context;
|
||||
QGridLayout* m_layout;
|
||||
std::vector<std::string> m_itemIds; // what the chips currently stand for
|
||||
std::vector<ItemChip*> m_chips;
|
||||
|
||||
163
src/ui/selection/ItemTooltip.cpp
Normal file
163
src/ui/selection/ItemTooltip.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "ItemTooltip.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
#include <QPalette>
|
||||
#include <QRect>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "DisplayName.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ItemProducers.h"
|
||||
#include "RecipeLineRow.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Gap between the cursor and the tooltip's corner, in device-independent pixels. Wide
|
||||
// enough that the cursor never covers the heading.
|
||||
const int kCursorOffsetPx = 16;
|
||||
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(
|
||||
const std::vector<RecipeIngredient>& ingredients)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
|
||||
{
|
||||
std::vector<RecipeLineRow::Amount> amounts;
|
||||
amounts.reserve(outputs.size());
|
||||
for (const RecipeOutput& output : outputs)
|
||||
{
|
||||
amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
// The screen the cursor is on, falling back to the primary screen when the position is
|
||||
// on none of them (a cursor between two screens of different heights).
|
||||
QRect availableScreenRect(const QPoint& globalPos)
|
||||
{
|
||||
const QScreen* screen = QGuiApplication::screenAt(globalPos);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
screen = QGuiApplication::primaryScreen();
|
||||
}
|
||||
return (screen != nullptr) ? screen->availableGeometry() : QRect();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
ItemTooltip::ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
||||
QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip)
|
||||
, m_context(context)
|
||||
, m_itemId(itemId)
|
||||
{
|
||||
// The palette's tooltip colors behind a themed border, so it reads as a tooltip
|
||||
// rather than as a stray window.
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
setAutoFillBackground(true);
|
||||
QPalette tooltipPalette = palette();
|
||||
tooltipPalette.setColor(QPalette::Window,
|
||||
tooltipPalette.color(QPalette::ToolTipBase));
|
||||
tooltipPalette.setColor(QPalette::WindowText,
|
||||
tooltipPalette.color(QPalette::ToolTipText));
|
||||
setPalette(tooltipPalette);
|
||||
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(6, 4, 6, 4);
|
||||
m_layout->setSpacing(2);
|
||||
}
|
||||
|
||||
void ItemTooltip::showAt(const QPoint& globalPos)
|
||||
{
|
||||
rebuild();
|
||||
adjustSize();
|
||||
|
||||
// Kept on the screen the cursor is on: a tooltip for a chip near the right or
|
||||
// bottom edge would otherwise be cut off.
|
||||
const QRect screen = availableScreenRect(globalPos);
|
||||
QPoint topLeft(globalPos.x() + kCursorOffsetPx, globalPos.y() + kCursorOffsetPx);
|
||||
if (screen.isValid())
|
||||
{
|
||||
topLeft.setX(qMax(screen.left(),
|
||||
qMin(topLeft.x(), screen.right() - width() + 1)));
|
||||
topLeft.setY(qMax(screen.top(),
|
||||
qMin(topLeft.y(), screen.bottom() - height() + 1)));
|
||||
}
|
||||
|
||||
move(topLeft);
|
||||
show();
|
||||
}
|
||||
|
||||
void ItemTooltip::rebuild()
|
||||
{
|
||||
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
// The heading names the item. For an input chip this is the only place it is named
|
||||
// at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION).
|
||||
QLabel* heading =
|
||||
new QLabel(QString::fromStdString(toDisplayName(m_itemId)), this);
|
||||
QFont headingFont = heading->font();
|
||||
headingFont.setBold(true);
|
||||
heading->setFont(headingFont);
|
||||
m_layout->addWidget(heading);
|
||||
heading->show();
|
||||
|
||||
const ItemProduction production =
|
||||
findItemProduction(m_itemId, *m_context.sim, *m_context.config);
|
||||
|
||||
// Scrap is salvaged from debris rather than crafted (REQ-RES-DEBRIS-DROP), so it
|
||||
// says where it comes from in place of the caption and lists nothing.
|
||||
QLabel* caption = new QLabel(production.origin == ItemOrigin::Salvaged
|
||||
? tr("Salvaged from debris")
|
||||
: tr("Produced by"), this);
|
||||
m_layout->addWidget(caption);
|
||||
caption->show();
|
||||
|
||||
if (production.origin == ItemOrigin::Undiscovered)
|
||||
{
|
||||
// Made somehow, but by nothing the player has unlocked -- named without being
|
||||
// shown a path they cannot take yet (REQ-UI-ITEM-TOOLTIP).
|
||||
QLabel* undiscovered = new QLabel(tr("Undiscovered"), this);
|
||||
m_layout->addWidget(undiscovered);
|
||||
undiscovered->show();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const RecipeDef* recipe : production.recipes)
|
||||
{
|
||||
RecipeLineRow::Spec spec;
|
||||
spec.building = recipe->building;
|
||||
spec.name = QString::fromStdString(toDisplayName(recipe->id));
|
||||
spec.inputs = toAmounts(recipe->inputs);
|
||||
spec.outputs = toAmounts(recipe->outputs);
|
||||
spec.durationSeconds = recipe->durationSeconds;
|
||||
|
||||
RecipeLineRow* line =
|
||||
new RecipeLineRow(m_context.itemIcons, m_context.buildingIcons, this);
|
||||
m_layout->addWidget(line);
|
||||
line->setLine(spec);
|
||||
}
|
||||
}
|
||||
36
src/ui/selection/ItemTooltip.h
Normal file
36
src/ui/selection/ItemTooltip.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QFrame>
|
||||
|
||||
#include "SelectionContext.h"
|
||||
|
||||
class QVBoxLayout;
|
||||
|
||||
// The tooltip an item chip shows on hover: the item's name, and every way the player
|
||||
// can currently produce it, each drawn as a recipe line (REQ-UI-ITEM-TOOLTIP).
|
||||
//
|
||||
// A window of its own rather than a Qt tooltip, because Qt's are text and this one is
|
||||
// drawn -- item squares, building chips and all. It is a Qt::ToolTip window, so it
|
||||
// floats above everything and takes no input.
|
||||
class ItemTooltip : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
// Rebuilds the content and shows the tooltip near the given screen position, kept
|
||||
// inside the screen. Rebuilt on every show because what produces an item changes as
|
||||
// the player unlocks recipes (REQ-LOCK-UI-RECIPE).
|
||||
void showAt(const QPoint& globalPos);
|
||||
|
||||
private:
|
||||
void rebuild();
|
||||
|
||||
SelectionContext m_context;
|
||||
std::string m_itemId;
|
||||
QVBoxLayout* m_layout;
|
||||
};
|
||||
@@ -45,13 +45,14 @@ void RecipeSelectionControl::setRecipeId(const std::string& recipeId)
|
||||
if (option.id == recipeId && !option.id.empty())
|
||||
{
|
||||
m_button->setText(option.caption);
|
||||
m_button->setToolTip(option.tooltip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No tooltip of its own: what the building has selected is drawn beneath the button
|
||||
// as the recipe summary, and where its items come from is reachable by hovering the
|
||||
// card's item chips (REQ-UI-SELECT-BUTTON, REQ-UI-ITEM-TOOLTIP).
|
||||
m_button->setText(m_type == BuildingType::Shipyard
|
||||
? tr("Select schematic")
|
||||
: tr("Select recipe"));
|
||||
m_button->setToolTip(QString());
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#include "RecipeSummaryRow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
#include <QPixmap>
|
||||
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the items' colored squares are drawn at on the summary line, in device-independent
|
||||
// pixels. Larger than the artwork they carry, which the square insets (REQ-UI-ITEM-ICON).
|
||||
const int kSummaryIconSizePx = 18;
|
||||
|
||||
// Adds a freshly built label to the summary and shows it.
|
||||
//
|
||||
// The show is what makes it count: a widget created under an already-visible parent
|
||||
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
|
||||
// add nothing to the size hint the panel measures itself against as soon as this
|
||||
// returns.
|
||||
void addAndShow(QHBoxLayout* layout, QLabel* label)
|
||||
{
|
||||
layout->addWidget(label);
|
||||
label->show();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
RecipeSummaryRow::RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
m_layout = new QHBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(4);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::setSummary(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs,
|
||||
double durationSeconds)
|
||||
{
|
||||
if (outputs.empty() && inputs.empty())
|
||||
{
|
||||
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
|
||||
// a change and shows the row again.
|
||||
m_inputs.clear();
|
||||
m_outputs.clear();
|
||||
m_durationSeconds = -1.0;
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
if (inputs == m_inputs && outputs == m_outputs
|
||||
&& durationSeconds == m_durationSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_inputs = inputs;
|
||||
m_outputs = outputs;
|
||||
m_durationSeconds = durationSeconds;
|
||||
rebuild(inputs, outputs, durationSeconds);
|
||||
show();
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::rebuild(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs,
|
||||
double durationSeconds)
|
||||
{
|
||||
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
addAmounts(inputs);
|
||||
if (!inputs.empty())
|
||||
{
|
||||
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||
addAndShow(m_layout, new QLabel(QString(rightArrow), this));
|
||||
}
|
||||
addAmounts(outputs);
|
||||
|
||||
if (durationSeconds > 0.0)
|
||||
{
|
||||
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
||||
addAndShow(m_layout, new QLabel(
|
||||
QStringLiteral("%1 %2").arg(middleDot).arg(
|
||||
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
|
||||
}
|
||||
m_layout->addStretch(1);
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
|
||||
{
|
||||
for (const Amount& entry : amounts)
|
||||
{
|
||||
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
||||
// is not an error: the square stands alone then, and only an item with no square
|
||||
// either falls back to its id in text.
|
||||
const QPixmap icon =
|
||||
m_itemIcons->getSquarePixmap(entry.itemId, kSummaryIconSizePx);
|
||||
if (!icon.isNull())
|
||||
{
|
||||
QLabel* iconLabel = new QLabel(this);
|
||||
iconLabel->setPixmap(icon);
|
||||
addAndShow(m_layout, iconLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
addAndShow(m_layout,
|
||||
new QLabel(QString::fromStdString(entry.itemId), this));
|
||||
}
|
||||
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this));
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ItemIconCache;
|
||||
class QHBoxLayout;
|
||||
|
||||
// What one production cycle does, on a line: each input item with its per-cycle amount,
|
||||
// an arrow, each output with its amount, and the cycle time
|
||||
// (REQ-UI-RECIPE-SUMMARY). It restates the selected recipe without the player having to
|
||||
// open the selection dialog, and it is the panel's only display of the cycle time.
|
||||
class RecipeSummaryRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Amount
|
||||
{
|
||||
std::string itemId;
|
||||
int amount = 0;
|
||||
|
||||
bool operator==(const Amount& other) const
|
||||
{
|
||||
return itemId == other.itemId && amount == other.amount;
|
||||
}
|
||||
};
|
||||
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||
// no icon file falls back to its id. Not owned.
|
||||
explicit RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||
|
||||
// Hides the row when there is nothing selected to produce, which is how a building
|
||||
// with no recipe shows no summary at all.
|
||||
void setSummary(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs, double durationSeconds);
|
||||
|
||||
private:
|
||||
void rebuild(const std::vector<Amount>& inputs, const std::vector<Amount>& outputs,
|
||||
double durationSeconds);
|
||||
void addAmounts(const std::vector<Amount>& amounts);
|
||||
|
||||
ItemIconCache* m_itemIcons;
|
||||
QHBoxLayout* m_layout;
|
||||
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
|
||||
// recipe actually changed.
|
||||
std::vector<Amount> m_inputs;
|
||||
std::vector<Amount> m_outputs;
|
||||
double m_durationSeconds = -1.0;
|
||||
};
|
||||
@@ -83,20 +83,11 @@ BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
|
||||
*getContext().config, target.recipeId, target.shipLayout);
|
||||
|
||||
// A shipyard's output is the ship itself, which never lands in an item buffer -- so
|
||||
// the summary shows one ship rather than an item id.
|
||||
info.durationSeconds = shipDef->schematic.productionTimeSeconds;
|
||||
if (target.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& placed : target.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* moduleDef =
|
||||
getContext().config->modules.findModuleDef(placed.moduleId);
|
||||
if (moduleDef)
|
||||
{
|
||||
info.durationSeconds += moduleDef->productionTimeSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
// the summary shows no output item at all. The time is the schematic's plus its
|
||||
// modules', from the same place the layout dialog's build cost reads it
|
||||
// (REQ-MOD-PRODUCTION-TIME).
|
||||
info.durationSeconds = computeShipyardProductionTimeSeconds(
|
||||
*getContext().config, target.recipeId, target.shipLayout);
|
||||
info.runsProduction = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user