From 44ec3c51f19527bd0491f70276b348417f78d0e2 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 18 Aug 2026 21:41:34 +0200 Subject: [PATCH] explain an item wherever the UI names one Every display naming an item now shows that item's production tooltip, not only the selection panel's chips: the header block stock, the total cost of a building multi-selection, the remaining scrap of a debris selection, the icons of every recipe summary, and a blueprint card's cost. ItemTooltip moves out of the selection panel and takes an ItemTooltipContext of its own -- an item is named all over the UI, and the panel's context carries visuals and a debug flag no tooltip reads. A recipe line wraps each icon with its amount so the pair can be pointed at as one statement, and attaches tooltips only where asked: a module button and the item tooltip itself draw the same line and stay silent. Hover only wherever the display sits on something the player clicks, whose click is not free to explain. Note that a recipe line on an option button can no longer be transparent to the mouse -- Qt never looks inside a transparent widget for the cursor -- so it relies on press propagation to keep picking the option. The header block stock loses world.building_blocks_tooltip, showing what every other block icon shows instead. The Expand button keeps no tooltip: its cost is painted into its face with nowhere to hang one, and the button is due to be removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- bin/app/data/config/world.toml | 1 - bin/test/data/config/world.toml | 1 - docs/requirements.md | 8 +-- src/lib/config/ConfigLoaderWorld.cpp | 6 -- src/lib/config/WorldConfig.h | 4 -- src/test/ConfigLoaderTest.cpp | 5 -- src/ui/BlueprintSelectionDialog.cpp | 32 +++++++-- src/ui/BlueprintSelectionDialog.h | 9 ++- src/ui/CMakeLists.txt | 3 + src/ui/HeaderBar.cpp | 45 +++++++------ src/ui/HeaderBar.h | 21 +++--- src/ui/{selection => }/ItemTooltip.cpp | 13 +++- src/ui/ItemTooltip.h | 36 +++++++++++ src/ui/ItemTooltipContext.h | 27 ++++++++ src/ui/MainWindow.cpp | 20 ++++-- src/ui/MainWindow.h | 4 ++ src/ui/RecipeLineRow.cpp | 68 ++++++++++++++------ src/ui/RecipeLineRow.h | 21 ++++++ src/ui/RecipeSelectionDialog.cpp | 15 ++++- src/ui/RecipeSelectionDialog.h | 11 ++-- src/ui/SchematicChoiceDialog.cpp | 14 ++-- src/ui/SchematicChoiceDialog.h | 13 ++-- src/ui/ShipLayoutDialog.cpp | 26 +++++--- src/ui/ShipLayoutDialog.h | 13 ++-- src/ui/selection/BufferedBuildingContent.cpp | 5 ++ src/ui/selection/CMakeLists.txt | 2 - src/ui/selection/DebrisContent.cpp | 6 ++ src/ui/selection/FieldMultiContent.cpp | 6 ++ src/ui/selection/ItemChip.cpp | 4 +- src/ui/selection/ItemTooltip.h | 29 --------- src/ui/selection/MultiBuildingContent.cpp | 6 ++ src/ui/selection/SelectionContext.h | 9 +++ src/ui/selection/StatRow.cpp | 5 ++ src/ui/selection/StatRow.h | 5 ++ 34 files changed, 336 insertions(+), 157 deletions(-) rename src/ui/{selection => }/ItemTooltip.cpp (85%) create mode 100644 src/ui/ItemTooltip.h create mode 100644 src/ui/ItemTooltipContext.h delete mode 100644 src/ui/selection/ItemTooltip.h diff --git a/bin/app/data/config/world.toml b/bin/app/data/config/world.toml index 10d42d3..634bc2d 100644 --- a/bin/app/data/config/world.toml +++ b/bin/app/data/config/world.toml @@ -11,7 +11,6 @@ tunnel_max_distance_tiles = 10 departure_interval_seconds = 20 orbit_factor = 0.8 rally_orbit_radius_tiles = 5.0 -building_blocks_tooltip = "Building blocks are the currency for construction. Spend them to place buildings and to expand the asteroid. Produce building blocks in your assemblers and deliver them to the HQ on a belt to grow your stock." artifact_tooltip = "Artifacts are the key to victory. Earn one by choosing the artifact reward when you destroy a set of enemy defence stations. Collect enough of them to win the game." [regions] diff --git a/bin/test/data/config/world.toml b/bin/test/data/config/world.toml index c817c92..51ba4b1 100644 --- a/bin/test/data/config/world.toml +++ b/bin/test/data/config/world.toml @@ -11,7 +11,6 @@ tunnel_max_distance_tiles = 10 departure_interval_seconds = 20 orbit_factor = 0.8 rally_orbit_radius_tiles = 5.0 -building_blocks_tooltip = "Spend building blocks to build; deliver them to the HQ to gain more." artifact_tooltip = "Choose the artifact reward when destroying enemy stations; collect enough to win." [regions] diff --git a/docs/requirements.md b/docs/requirements.md index 8a1d9e1..0591c46 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -381,7 +381,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro Each option additionally displays a vertical list of recipe lines labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation. - Each recipe is shown as a **recipe line** of the same two-row card the item production tooltip uses (REQ-UI-ITEM-TOOLTIP): the icon of the building that runs it (REQ-UI-BUILD-ICON) and the recipe's name — by its `id`, using the same display convention as the assembler recipe-selection dialog — on the first row, and the recipe drawn as the recipe summary draws it (REQ-UI-RECIPE-SUMMARY) on the second. The lines are sorted alphabetically by recipe name. The line says everything there is to say about the recipe, so nothing in this list carries a tooltip describing it, as in the selection dialog (REQ-UI-SELECT-OPTIONS); the items the line names do explain themselves on hover, the click belonging to the option the player is choosing (REQ-UI-ITEM-VALUE-TOOLTIP). If no recipes would be newly unlocked, the list shows "None". + Each recipe is shown as a **recipe line** of the same two-row card the item production tooltip uses (REQ-UI-ITEM-TOOLTIP): the icon of the building that runs it (REQ-UI-BUILD-ICON) and the recipe's name — by its `id`, using the same display convention as the assembler recipe-selection dialog — on the first row, and the recipe drawn as the recipe summary draws it (REQ-UI-RECIPE-SUMMARY) on the second. The lines are sorted alphabetically by recipe name. The line says everything there is to say about the recipe, so nothing in this list carries a tooltip describing it, as in the selection dialog (REQ-UI-SELECT-OPTIONS); the items the line names do explain themselves, on hover or click: the card is not itself clicked, the option being taken with the button beneath it (REQ-UI-ITEM-VALUE-TOOLTIP). If no recipes would be newly unlocked, the list shows "None". **The dialog cannot be dismissed.** Clicking an option is the only thing that closes it: it has no close button, no Cancel, neither Escape nor Q dismisses it, and a click outside it does nothing (REQ-UI-DIALOG-DISMISS). The drop is a reward the player has earned by destroying the station set, and every way out of the dialog would have to either forfeit it or pick an option the player did not — so there is no way out but choosing. The dialog is modal and the game is paused meanwhile, so nothing is waiting on the decision. @@ -445,13 +445,13 @@ Several UI elements carry a tooltip: the header bar's artifact count (REQ-UI-ART - REQ-UI-TOOLTIP-DISMISS: A tooltip stays visible until the pointer leaves **both the tooltip and the element it belongs to**, however it was triggered. It does **not time out**: there is no display duration after which it vanishes on its own, so a tooltip listing several recipe lines (REQ-UI-ITEM-TOOLTIP) can be read at whatever pace the player needs. The tooltip is itself part of the area that keeps it open — moving the pointer off the element and onto the tooltip does not hide it — which is what fixes where it is placed: its **top-left corner sits at the pointer**, at the position the pointer had the moment the tooltip was triggered. The pointer therefore starts on the tooltip's own corner and reaches the rest of it without crossing a gap. A tooltip that would run off the screen is pushed back onto it, which only moves it further over the pointer. Once the pointer is outside both, the tooltip hides. Nothing else dismisses it, and nothing else needs to: a click elsewhere is a click the pointer has already travelled to, so the tooltip is gone before it lands. - REQ-UI-ITEM-VALUE-TOOLTIP: **Wherever the UI names an item, that spot explains the item.** An item is named by its icon, and it is named in three forms (REQ-UI-ITEM-ICON): as an **item chip**, as a **value** — a number followed by the bare inline icon standing in for the item's name — and as an **icon on its colored square within a recipe summary** (REQ-UI-RECIPE-SUMMARY), each with its per-cycle amount. All three show that item's production tooltip (REQ-UI-ITEM-TOOLTIP). The player can therefore ask what an item is wherever it is named, rather than only where it happens to be drawn as a chip. - The **whole value carries the tooltip** — the amount and the icon together, in every form: the two are one statement, and an icon alone is a target no taller than a line of text. Nothing else on the line is part of it: the caption beside a value (`Remaining`, `Total cost`) describes the number rather than the item, and a recipe summary's arrow, separators and cycle time belong to no item at all. None of these displays has a click action of its own, so a click shows the tooltip at once, as it does on a chip (REQ-UI-TOOLTIP-TRIGGER). This covers the header bar's building blocks stock (REQ-UI-BLOCKS-ICON), the building multi-selection's total cost (REQ-UI-MULTI-SELECTION), the remaining scrap of the debris panel and the field multi-selection (REQ-UI-DEBRIS-PANEL, REQ-UI-FIELD-MULTI-SELECTION), and every input and output icon of the selection panel's recipe summary, a Shipyard's schematic materials included (REQ-UI-RECIPE-SUMMARY). For a value it holds whether or not the icon file exists: where a missing icon leaves the fallback text (`Stock: Blocks`, `Scrap remaining`), the value still states an amount of an item and still explains it. + The **whole value carries the tooltip** — the amount and the icon together, in every form: the two are one statement, and an icon alone is a target no taller than a line of text. Nothing else on the line is part of it: the caption beside a value (`Remaining`, `Total cost`) describes the number rather than the item, and a recipe summary's arrow, separators and cycle time belong to no item at all. None of these displays has a click action of its own, so a click shows the tooltip at once, as it does on a chip (REQ-UI-TOOLTIP-TRIGGER). This covers the header bar's building blocks stock (REQ-UI-BLOCKS-ICON), the building multi-selection's total cost (REQ-UI-MULTI-SELECTION), the remaining scrap of the debris panel and the field multi-selection (REQ-UI-DEBRIS-PANEL, REQ-UI-FIELD-MULTI-SELECTION), every input and output icon of the selection panel's recipe summary, a Shipyard's schematic materials included (REQ-UI-RECIPE-SUMMARY), and the layout configuration dialog's build cost line, which states a ship's materials in that same form (REQ-MOD-UI-DIALOG). For a value it holds whether or not the icon file exists: where a missing icon leaves the fallback text (`Stock: Blocks`, `Scrap remaining`), the value still states an amount of an item and still explains it. **An item named on something that already carries a tooltip of its own carries none.** One display shows one tooltip, and the one it was given for its own sake is the one that wins: the building block cost on a build button says nothing about blocks, because the button is there to explain the building (REQ-UI-BUILD-COST, REQ-UI-BUILD-TOOLTIP). The module selection buttons are excluded on the same ground even though a module without a `modules.toml` description leaves its button silent (REQ-MOD-UI-DIALOG, REQ-MOD-UI-MODULE-TOOLTIP): the button is a described control by design, and whether one module happens to carry the text is a gap in the config rather than a decision about the UI — the same material icon must not explain itself on one button and not on the one beside it. - **Everywhere else the item is explained, on a clickable control or not**: the asteroid expansion button's cost (REQ-UI-EXPAND-BUTTON), the blueprint cards' costs (REQ-UI-BLUEPRINT-CARD), the recipe summaries on the selection dialog's option buttons (REQ-UI-SELECT-OPTIONS), and the recipe lines on the unlock-choice dialog's options (REQ-DEF-SCHEMATIC-DROP). These controls say what they do and nothing about the items they name, and the two answers do not compete: an option button says what a recipe makes, an item tooltip says where an input comes from. + **Everywhere else the item is explained, on a clickable control or not**: the asteroid expansion button's cost (REQ-UI-EXPAND-BUTTON), the blueprint cards' costs (REQ-UI-BLUEPRINT-CARD), the recipe summaries on the selection dialog's option buttons (REQ-UI-SELECT-OPTIONS), and the recipe lines of the unlock-choice dialog's options (REQ-DEF-SCHEMATIC-DROP). These say what they do and nothing about the items they name, and the two answers do not compete: an option button says what a recipe makes, an item tooltip says where an input comes from. - **On a clickable control the tooltip shows on hover only.** The click belongs to the control — it buys the expansion, picks the blueprint, selects the recipe, takes the unlock — so it cannot also be the gesture that explains (REQ-UI-TOOLTIP-TRIGGER). A click shows the tooltip at once only where the item is named on something with no click action of its own: an item chip, a value in a row, the selection panel's recipe summary. + **On a clickable control the tooltip shows on hover only.** The click belongs to the control — it buys the expansion, picks the blueprint, selects the recipe — so it cannot also be the gesture that explains (REQ-UI-TOOLTIP-TRIGGER). A click shows the tooltip at once wherever the item is named on something with no click action of its own: an item chip, a value in a row, the selection panel's recipe summary, and the recipe lines of an unlock-choice option, whose card is chosen through a button of its own rather than by being clicked (REQ-DEF-SCHEMATIC-DROP). **An item named inside a tooltip carries none either.** The recipe lines of the item production tooltip name items as a recipe summary does, but a tooltip opened from a tooltip would replace the one being read, since only one is ever on screen and the pointer holds up only what it rests on (REQ-UI-TOOLTIP-TRIGGER, REQ-UI-TOOLTIP-DISMISS). diff --git a/src/lib/config/ConfigLoaderWorld.cpp b/src/lib/config/ConfigLoaderWorld.cpp index 4e55b96..23bc171 100644 --- a/src/lib/config/ConfigLoaderWorld.cpp +++ b/src/lib/config/ConfigLoaderWorld.cpp @@ -27,12 +27,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path) cfg.orbitFactor = utility::requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor"); cfg.rallyOrbitRadius_tiles = utility::requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles"); - if (const std::optional tip = - tbl["world"]["building_blocks_tooltip"].value()) - { - cfg.buildingBlocksTooltip = *tip; - } - if (const std::optional tip = tbl["world"]["artifact_tooltip"].value()) { diff --git a/src/lib/config/WorldConfig.h b/src/lib/config/WorldConfig.h index 1e022e5..6447b41 100644 --- a/src/lib/config/WorldConfig.h +++ b/src/lib/config/WorldConfig.h @@ -79,10 +79,6 @@ struct WorldConfig double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius) double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point) - // Optional hover-tooltip for the header building blocks stock display - // (REQ-UI-BLOCKS-TOOLTIP). Presentation-only; the simulation ignores it. - std::optional buildingBlocksTooltip; - // Optional hover-tooltip for the header artifact count display // (REQ-UI-ARTIFACTS-TOOLTIP). Presentation-only; the simulation ignores it. std::optional artifactTooltip; diff --git a/src/test/ConfigLoaderTest.cpp b/src/test/ConfigLoaderTest.cpp index 1b50f38..dcff55a 100644 --- a/src/test/ConfigLoaderTest.cpp +++ b/src/test/ConfigLoaderTest.cpp @@ -84,11 +84,6 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0)); REQUIRE(cfg.world.scrapPerThreat == Approx(1.0)); - // Optional header building blocks tooltip (REQ-UI-BLOCKS-TOOLTIP). - REQUIRE(cfg.world.buildingBlocksTooltip.has_value()); - REQUIRE(*cfg.world.buildingBlocksTooltip == - "Spend building blocks to build; deliver them to the HQ to gain more."); - // Optional header artifact tooltip (REQ-UI-ARTIFACTS-TOOLTIP). REQUIRE(cfg.world.artifactTooltip.has_value()); REQUIRE(*cfg.world.artifactTooltip == diff --git a/src/ui/BlueprintSelectionDialog.cpp b/src/ui/BlueprintSelectionDialog.cpp index 5b5cb4f..a6d50e9 100644 --- a/src/ui/BlueprintSelectionDialog.cpp +++ b/src/ui/BlueprintSelectionDialog.cpp @@ -30,6 +30,7 @@ #include "BlueprintLibrary.h" #include "IconCaption.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" #include "TooltipTrigger.h" namespace @@ -113,9 +114,10 @@ namespace return face; } - // A composed card face and the size to show it at; the button needs both, and only - // the composer knows the size it arrived at. - struct CardFace { QIcon icon; QSize size; }; + // A composed card face, the size to show it at, and where in it the cost ended up: + // the button needs the first two, and only the composer knows the third, which is + // what the cost's tooltip is hung on (REQ-UI-ITEM-VALUE-TOOLTIP). + struct CardFace { QIcon icon; QSize size; QRect costRect; }; // The two-mode face of one card. The modes differ only in the text color, so an // unaffordable card greys itself when Qt swaps the pixmap on the disabled button @@ -142,17 +144,23 @@ namespace textColor, dimColor, faceSize); result.icon.addPixmap(face, mode); result.size = result.size.expandedTo(getLogicalSize(face)); + // Where composeCardFace put it: bottom left, its own width. Both modes + // compose the same text, so both arrive at the same rect. + const QSize costSize = getLogicalSize(costPixmap); + result.costRect = QRect(0, faceSize.height() - costSize.height(), + costSize.width(), costSize.height()); } return result; } } BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library, - ItemIconCache* itemIcons, + const ItemTooltipContext& context, QWidget* parent) : ModalDialog(parent) , m_library(library) - , m_itemIcons(itemIcons) + , m_context(context) + , m_itemIcons(context.itemIcons) { QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx); @@ -264,6 +272,20 @@ void BlueprintSelectionDialog::rebuildGrid() body->setEnabled(m_library->getCanAfford(i)); cardLayout->addWidget(body); + // The cost is painted into the face, so what explains the item it names is a + // child of its own laid over that band -- placed by hand like the delete icon + // below (REQ-UI-ITEM-VALUE-TOOLTIP). The face is centered in the button, which + // is what the offset accounts for. A child of the body rather than a sibling, so + // that the press it does not handle still reaches the button and picks the + // blueprint; hover only, for the same reason (REQ-UI-TOOLTIP-TRIGGER). + QWidget* costArea = new QWidget(body); + const QPoint faceOrigin((cardSize.width() - face.size.width()) / 2, + (cardSize.height() - face.size.height()) / 2); + costArea->setGeometry(QRect(faceOrigin + face.costRect.topLeft(), + face.costRect.size())); + ItemTooltip::attachTo(*costArea, m_context, kBlockItemId, + TooltipTrigger::Trigger::HoverOnly); + // A sibling of the body rather than one of its children, and outside the // layout: Qt disables a widget's children along with it, and the delete icon // must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE). diff --git a/src/ui/BlueprintSelectionDialog.h b/src/ui/BlueprintSelectionDialog.h index a8c6ce5..1a9ed8f 100644 --- a/src/ui/BlueprintSelectionDialog.h +++ b/src/ui/BlueprintSelectionDialog.h @@ -2,6 +2,7 @@ #include +#include "ItemTooltipContext.h" #include "ModalDialog.h" class BlueprintLibrary; @@ -23,8 +24,11 @@ class BlueprintSelectionDialog : public ModalDialog Q_OBJECT public: - // Neither the library nor the icon cache is owned; both outlive the dialog. - BlueprintSelectionDialog(BlueprintLibrary* library, ItemIconCache* itemIcons, + // The context's item cache draws the block icon beside each cost, and the rest of it + // lets that cost explain the item it names (REQ-UI-ITEM-VALUE-TOOLTIP). Neither the + // library nor anything in the context is owned; both outlive the dialog. + BlueprintSelectionDialog(BlueprintLibrary* library, + const ItemTooltipContext& context, QWidget* parent = nullptr); std::optional getChosenIndex() const; @@ -37,6 +41,7 @@ private: void onDeleteClicked(int index); BlueprintLibrary* m_library; + ItemTooltipContext m_context; ItemIconCache* m_itemIcons; QWidget* m_gridContainer; QGridLayout* m_grid; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index b969741..5ab66b5 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -30,6 +30,8 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.h ${CMAKE_CURRENT_SOURCE_DIR}/Tooltip.h + ${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.h + ${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltipContext.h ${CMAKE_CURRENT_SOURCE_DIR}/TooltipTrigger.h ${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.h @@ -71,6 +73,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Tooltip.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TooltipTrigger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.cpp diff --git a/src/ui/HeaderBar.cpp b/src/ui/HeaderBar.cpp index e771068..1862593 100644 --- a/src/ui/HeaderBar.cpp +++ b/src/ui/HeaderBar.cpp @@ -16,6 +16,7 @@ #include "EventManager.h" #include "IconCaption.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" #include "Simulation.h" #include "SpeedChangeRequestedEvent.h" #include "Tick.h" @@ -24,11 +25,9 @@ const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 }; const int HeaderBar::kSpeedCount = 5; -HeaderBar::HeaderBar(const Simulation* sim, const GameConfig* config, - ItemIconCache* itemIcons, QWidget* parent) +HeaderBar::HeaderBar(const ItemTooltipContext& context, QWidget* parent) : QWidget(parent) - , m_itemIcons(itemIcons) - , m_sim(sim) + , m_context(context) { QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(8, 4, 8, 4); @@ -37,21 +36,21 @@ HeaderBar::HeaderBar(const Simulation* sim, const GameConfig* config, m_timeLabel = new QLabel("00:00", this); // Both displays state a value and do nothing when clicked, so a click brings the // tooltip up at once rather than waiting the hover out (REQ-UI-TOOLTIP-TRIGGER). + // The stock states an amount of an item, so what it shows is that item's production + // tooltip, as every other display naming an item does + // (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-VALUE-TOOLTIP). m_blocksLabel = new QLabel(this); - if (config->world.buildingBlocksTooltip) - { - TooltipTrigger::attachText( - *m_blocksLabel, - QString::fromStdString(*config->world.buildingBlocksTooltip), - TooltipTrigger::Trigger::HoverAndClick); - } + ItemTooltip::attachTo(*m_blocksLabel, m_context, kBlockItemId, + TooltipTrigger::Trigger::HoverAndClick); updateBlocksLabel(); + // An artifact count is no item amount, so it has no production path to show and + // states configured text instead (REQ-UI-ARTIFACTS-TOOLTIP). m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this); - if (config->world.artifactTooltip) + if (m_context.config->world.artifactTooltip) { TooltipTrigger::attachText( *m_artifactsLabel, - QString::fromStdString(*config->world.artifactTooltip), + QString::fromStdString(*m_context.config->world.artifactTooltip), TooltipTrigger::Trigger::HoverAndClick); } m_bossWaveLabel = new QLabel(tr("Boss Wave #1"), this); @@ -108,7 +107,7 @@ HeaderBar::~HeaderBar() void HeaderBar::handleEvent(std::shared_ptr /*event*/) { - const int totalSeconds = static_cast(ticksToSeconds(m_sim->getCurrentTick())); + const int totalSeconds = static_cast(ticksToSeconds(m_context.sim->getCurrentTick())); m_timeLabel->setText( QString("%1:%2") .arg(totalSeconds / 60, 2, 10, QChar('0')) @@ -128,9 +127,9 @@ void HeaderBar::handleEvent(std::shared_ptr /*e void HeaderBar::updateBlocksLabel() { - const int blocks = m_sim->getBuildingBlocksStock(); + const int blocks = m_context.sim->getBuildingBlocksStock(); - const QPixmap icon = m_itemIcons->getInlineIcon(kBlockItemId, font()); + const QPixmap icon = m_context.itemIcons->getInlineIcon(kBlockItemId, font()); if (icon.isNull()) { // Fallback text form when no building_block icon exists (REQ-UI-BLOCKS-ICON). @@ -144,12 +143,12 @@ void HeaderBar::updateBlocksLabel() void HeaderBar::updateExpandButton() { - const int blocks = m_sim->getBuildingBlocksStock(); - const int expansionCost = m_sim->getCurrentExpansionCost(); + const int blocks = m_context.sim->getBuildingBlocksStock(); + const int expansionCost = m_context.sim->getCurrentExpansionCost(); m_expandButton->setEnabled(blocks >= expansionCost); - const QPixmap icon = m_itemIcons->getInlineIcon(kBlockItemId, font()); + const QPixmap icon = m_context.itemIcons->getInlineIcon(kBlockItemId, font()); if (icon.isNull()) { // Fallback text form when no building_block icon exists (REQ-UI-EXPAND-BUTTON). @@ -188,12 +187,12 @@ void HeaderBar::handleEvent(std::shared_ptr event) void HeaderBar::handleEvent(std::shared_ptr /*event*/) { - const Tick countdownTicks = m_sim->getBossCountdownTicks(); + const Tick countdownTicks = m_context.sim->getBossCountdownTicks(); const int bossSeconds = static_cast( ticksToSeconds(countdownTicks > 0 ? countdownTicks : 0)); m_bossWaveLabel->setText( tr("Boss Wave #%1") - .arg(m_sim->getBossWaveCounter())); + .arg(m_context.sim->getBossWaveCounter())); m_nextBossLabel->setText( tr("Next boss: %1:%2") .arg(bossSeconds / 60) @@ -204,8 +203,8 @@ void HeaderBar::handleEvent(std::shared_ptr /*e { m_artifactsLabel->setText( tr("Artifacts: %1/%2") - .arg(m_sim->getArtifactCount()) - .arg(m_sim->getConfig().world.artifacts.artifactWinCount)); + .arg(m_context.sim->getArtifactCount()) + .arg(m_context.sim->getConfig().world.artifacts.artifactWinCount)); } void HeaderBar::onSpeedButton(int index) diff --git a/src/ui/HeaderBar.h b/src/ui/HeaderBar.h index 267797e..108a369 100644 --- a/src/ui/HeaderBar.h +++ b/src/ui/HeaderBar.h @@ -14,6 +14,7 @@ #include "ExpansionCostChangedEvent.h" #include "GameConfig.h" #include "GameSpeedChangedEvent.h" +#include "ItemTooltipContext.h" #include "Tick.h" #include "TickAdvancedEvent.h" @@ -33,11 +34,12 @@ class HeaderBar : public QWidget, Q_OBJECT public: - // itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); used to - // show the building_block icon in the stock display and expand button - // (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON). Not owned; must outlive this widget. - HeaderBar(const Simulation* sim, const GameConfig* config, - ItemIconCache* itemIcons, QWidget* parent = nullptr); + // The context carries the simulation the bar reads, the config, and the icon caches + // -- the item cache draws the building_block icon in the stock display and the expand + // button (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON), and the stock display explains + // that item with the cache's help (REQ-UI-ITEM-VALUE-TOOLTIP). Nothing in it is + // owned; all of it must outlive this widget. + explicit HeaderBar(const ItemTooltipContext& context, QWidget* parent = nullptr); ~HeaderBar() override; private slots: @@ -69,11 +71,10 @@ private: QPushButton* m_expandButton; std::vector m_speedButtons; - ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow. - - // The simulation is the single source of truth for everything the header - // displays; the change events are only refresh signals. - const Simulation* m_sim; + // Not owned; all of it lives in MainWindow. The simulation it names is the single + // source of truth for everything the header displays; the change events are only + // refresh signals. + ItemTooltipContext m_context; static const double kSpeeds[]; static const int kSpeedCount; diff --git a/src/ui/selection/ItemTooltip.cpp b/src/ui/ItemTooltip.cpp similarity index 85% rename from src/ui/selection/ItemTooltip.cpp rename to src/ui/ItemTooltip.cpp index 74e482a..9a5b119 100644 --- a/src/ui/selection/ItemTooltip.cpp +++ b/src/ui/ItemTooltip.cpp @@ -29,7 +29,14 @@ std::vector toAmounts( } // namespace -ItemTooltip::ItemTooltip(const SelectionContext& context, const std::string& itemId, +void ItemTooltip::attachTo(QWidget& target, const ItemTooltipContext& context, + const std::string& itemId, TooltipTrigger::Trigger trigger) +{ + ItemTooltip* tooltip = new ItemTooltip(context, itemId, &target); + TooltipTrigger::attach(target, *tooltip, trigger); +} + +ItemTooltip::ItemTooltip(const ItemTooltipContext& context, const std::string& itemId, QWidget* parent) : Tooltip(parent) , m_context(context) @@ -83,7 +90,9 @@ void ItemTooltip::refreshContent() spec.durationSeconds = recipe->durationSeconds; // Boxed, because an item with several producers stacks several of these and a run - // of bare lines reads as one field of icons (REQ-UI-ITEM-TOOLTIP). + // of bare lines reads as one field of icons (REQ-UI-ITEM-TOOLTIP). The items it + // names carry no tooltip of their own: this is already one + // (REQ-UI-ITEM-VALUE-TOOLTIP). RecipeLineRow* line = new RecipeLineRow(m_context.itemIcons, m_context.buildingIcons, this); line->setCardChrome(true); diff --git a/src/ui/ItemTooltip.h b/src/ui/ItemTooltip.h new file mode 100644 index 0000000..f0896ed --- /dev/null +++ b/src/ui/ItemTooltip.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "ItemTooltipContext.h" +#include "Tooltip.h" +#include "TooltipTrigger.h" + +// The tooltip an item's name carries: the item, and every way the player can currently +// produce it, each drawn as a recipe line (REQ-UI-ITEM-TOOLTIP). +// +// Its content is drawn -- item squares, building chips and all -- rather than written, +// which is what it adds to the plain text tooltip it derives from. Rebuilt on every show, +// because what produces an item changes as the player unlocks recipes +// (REQ-LOCK-UI-RECIPE). +class ItemTooltip : public Tooltip +{ + Q_OBJECT + +public: + // Gives one display the tooltip of the item it names (REQ-UI-ITEM-VALUE-TOOLTIP). + // The tooltip is parented to the target and dies with it. HoverOnly wherever the + // target sits on something the player clicks, whose click is not free to explain. + static void attachTo(QWidget& target, const ItemTooltipContext& context, + const std::string& itemId, TooltipTrigger::Trigger trigger); + + ItemTooltip(const ItemTooltipContext& context, const std::string& itemId, + QWidget* parent = nullptr); + +protected: + void refreshContent() override; + +private: + ItemTooltipContext m_context; + std::string m_itemId; +}; diff --git a/src/ui/ItemTooltipContext.h b/src/ui/ItemTooltipContext.h new file mode 100644 index 0000000..43eabfd --- /dev/null +++ b/src/ui/ItemTooltipContext.h @@ -0,0 +1,27 @@ +#pragma once + +struct GameConfig; +class BuildingIconCache; +class ItemIconCache; +class Simulation; + +// Everything an item production tooltip reads (REQ-UI-ITEM-TOOLTIP): the simulation it +// asks which recipes the player has unlocked, the immutable config holding the recipes +// themselves, and the two icon caches its recipe lines are drawn from. +// +// Its own bundle rather than the selection panel's (SelectionContext), because an item is +// named all over the UI -- the header bar, the dialogs, the panel -- and every one of +// those places has to be able to explain it (REQ-UI-ITEM-VALUE-TOOLTIP). The panel's +// context carries visuals and the debug flag besides, which no tooltip reads. +// +// Nothing here is owned: a view onto objects living in MainWindow, which it must not +// outlive. +struct ItemTooltipContext +{ + // Const because a tooltip only ever reads: what produces an item is a question, not + // a command. + const Simulation* sim = nullptr; + const GameConfig* config = nullptr; + ItemIconCache* itemIcons = nullptr; + BuildingIconCache* buildingIcons = nullptr; +}; diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 64e2d70..ace1de1 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -60,7 +60,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, m_buildingIcons = std::make_unique( QDir::cleanPath(configDirPath + "/../icons/buildings")); - m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this); + m_headerBar = new HeaderBar(getItemTooltipContext(), this); m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir, m_itemIcons.get(), m_replay.get(), this); @@ -237,7 +237,7 @@ void MainWindow::handleEvent(std::shared_ptrchoices, m_sim->getConfig().recipes, - m_itemIcons.get(), m_buildingIcons.get(), m_modalLayer); + getItemTooltipContext(), m_modalLayer); m_modalLayer->execute(dialog); // The command goes out unconditionally because the dialog cannot be dismissed: it @@ -330,11 +330,11 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId, } } - ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout, + ShipLayoutDialog dialog(schematicId, currentLayout, m_layoutBlueprints, std::move(unlockedModuleIds), m_gameWorldView->isDebugDrawEnabled(), - m_itemIcons.get(), m_modalLayer); + getItemTooltipContext(), m_modalLayer); // 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. if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted @@ -428,8 +428,8 @@ void MainWindow::handleEvent(std::shared_ptrexecute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted && dialog.getChosenId().has_value()) { @@ -491,9 +491,15 @@ void MainWindow::handleEvent(std::shared_ptrgetConfig(), m_itemIcons.get(), + m_buildingIcons.get() }; +} + void MainWindow::showBlueprintSelectionDialog() { - BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), + BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), getItemTooltipContext(), m_modalLayer); if (m_modalLayer->execute(dialog) == QDialog::Accepted && dialog.getChosenIndex().has_value()) diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index 73a454e..c2d7ebe 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -15,6 +15,7 @@ #include "FloatingLayoutInvalidatedEvent.h" #include "GameConfig.h" #include "GameOverEvent.h" +#include "ItemTooltipContext.h" #include "LayoutDialogRequestedEvent.h" #include "ModalLayer.h" #include "WinEvent.h" @@ -93,6 +94,9 @@ private: // keeps the dim continuous when a confirmed save hands straight over to this dialog // (REQ-UI-MODAL-DIM). void showBlueprintSelectionDialog(); + // What every display naming an item needs to explain it (REQ-UI-ITEM-VALUE-TOOLTIP). + // Assembled here because this is where the simulation and the icon caches live. + ItemTooltipContext getItemTooltipContext() const; // Places the widgets floating over the game world view, in one ordered pass // (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent. void layoutPanels(); diff --git a/src/ui/RecipeLineRow.cpp b/src/ui/RecipeLineRow.cpp index 9360423..1550955 100644 --- a/src/ui/RecipeLineRow.cpp +++ b/src/ui/RecipeLineRow.cpp @@ -10,6 +10,7 @@ #include "BuildingIconCache.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" namespace { @@ -105,6 +106,13 @@ void RecipeLineRow::setCardChrome(bool enabled) : QMargins(0, 0, 0, 0)); } +void RecipeLineRow::setItemTooltips(const ItemTooltipContext& context, + TooltipTrigger::Trigger trigger) +{ + m_tooltipContext = context; + m_tooltipTrigger = trigger; +} + void RecipeLineRow::setLine(const Spec& spec) { if (spec.isEmpty()) @@ -199,25 +207,45 @@ void RecipeLineRow::addAmounts(const std::vector& amounts) { addAndShow(m_amountsLayout, new QLabel(QStringLiteral("+"), m_amountsRow)); } - - // 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(m_amountsRow); - iconLabel->setPixmap(icon); - addAndShow(m_amountsLayout, iconLabel); - } - else - { - addAndShow(m_amountsLayout, - new QLabel(QString::fromStdString(entry.itemId), m_amountsRow)); - } - addAndShow(m_amountsLayout, - new QLabel(QString::number(entry.amount), m_amountsRow)); + addAmount(entry); } } + +void RecipeLineRow::addAmount(const Amount& entry) +{ + // A widget of its own around the pair, so what the player points at is the whole + // statement -- this many of that item -- rather than an icon the height of a line of + // text (REQ-UI-ITEM-VALUE-TOOLTIP). Its spacing is the row's own, so the line reads + // exactly as it did when the two labels sat in the row directly. + QWidget* pair = new QWidget(m_amountsRow); + QHBoxLayout* pairLayout = new QHBoxLayout(pair); + pairLayout->setContentsMargins(0, 0, 0, 0); + pairLayout->setSpacing(m_amountsLayout->spacing()); + + // 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(pair); + iconLabel->setPixmap(icon); + addAndShow(pairLayout, iconLabel); + } + else + { + addAndShow(pairLayout, + new QLabel(QString::fromStdString(entry.itemId), pair)); + } + addAndShow(pairLayout, new QLabel(QString::number(entry.amount), pair)); + + if (m_tooltipContext.has_value()) + { + ItemTooltip::attachTo(*pair, *m_tooltipContext, entry.itemId, m_tooltipTrigger); + } + + m_amountsLayout->addWidget(pair); + pair->show(); +} diff --git a/src/ui/RecipeLineRow.h b/src/ui/RecipeLineRow.h index e6b1832..c8358a9 100644 --- a/src/ui/RecipeLineRow.h +++ b/src/ui/RecipeLineRow.h @@ -8,7 +8,9 @@ #include #include "BuildingType.h" +#include "ItemTooltipContext.h" #include "RecipesConfig.h" +#include "TooltipTrigger.h" class BuildingIconCache; class ItemIconCache; @@ -104,9 +106,24 @@ public: // by what holds it. void setCardChrome(bool enabled); + // Lets every amount on the line explain the item it names, the number and the icon + // together as one target (REQ-UI-ITEM-VALUE-TOOLTIP). Off by default, because the + // line is silent wherever what holds it carries a tooltip of its own -- a module + // button (REQ-MOD-UI-MODULE-TOOLTIP) -- or is itself a tooltip (REQ-UI-ITEM-TOOLTIP). + // HoverOnly where the line sits on something the player clicks, whose click is not + // free to explain (REQ-UI-TOOLTIP-TRIGGER). + // + // Call before the first setLine(): the tooltips are attached as the line is built, + // and an unchanged spec is not rebuilt. + void setItemTooltips(const ItemTooltipContext& context, + TooltipTrigger::Trigger trigger); + private: void rebuild(const Spec& spec); void addAmounts(const std::vector& amounts); + // The icon and the number of one item, side by side in a widget of their own so the + // pair can be pointed at as one thing (REQ-UI-ITEM-VALUE-TOOLTIP). + void addAmount(const Amount& entry); ItemIconCache* m_itemIcons; BuildingIconCache* m_buildingIcons; @@ -121,4 +138,8 @@ private: // What the row currently shows, so a refresh at tick rate rebuilds it only when the // recipe actually changed. Spec m_spec; + // Unset on a line whose items say nothing, which is most of them. + std::optional m_tooltipContext; + TooltipTrigger::Trigger m_tooltipTrigger = + TooltipTrigger::Trigger::HoverOnly; }; diff --git a/src/ui/RecipeSelectionDialog.cpp b/src/ui/RecipeSelectionDialog.cpp index 31c70a6..98de7d9 100644 --- a/src/ui/RecipeSelectionDialog.cpp +++ b/src/ui/RecipeSelectionDialog.cpp @@ -106,7 +106,7 @@ namespace RecipeSelectionDialog::RecipeSelectionDialog( const std::vector& options, - const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons, + const QString& title, const ItemTooltipContext& context, QWidget* parent) : ModalDialog(parent) { @@ -156,8 +156,17 @@ RecipeSelectionDialog::RecipeSelectionDialog( nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true); face->addWidget(nameLabel); - RecipeLineRow* line = new RecipeLineRow(itemIcons, buildingIcons, button); - line->setAttribute(Qt::WA_TransparentForMouseEvents, true); + // The button face says what the option makes; what it does not say is where + // an input comes from, and that its items answer themselves. Hover only: the + // click here picks the option (REQ-UI-ITEM-VALUE-TOOLTIP). + // + // Unlike the name above it, the line is not transparent to the mouse: Qt + // never looks inside a transparent widget for the cursor, so its items could + // not be hovered if it were. The press it does not handle propagates to the + // button as it would from any other child, so the option still picks. + RecipeLineRow* line = + new RecipeLineRow(context.itemIcons, context.buildingIcons, button); + line->setItemTooltips(context, TooltipTrigger::Trigger::HoverOnly); face->addWidget(line); line->setLine(option.line); } diff --git a/src/ui/RecipeSelectionDialog.h b/src/ui/RecipeSelectionDialog.h index 7c3b549..3c42af8 100644 --- a/src/ui/RecipeSelectionDialog.h +++ b/src/ui/RecipeSelectionDialog.h @@ -7,12 +7,11 @@ #include #include "BuildingType.h" +#include "ItemTooltipContext.h" #include "ModalDialog.h" #include "RecipeLineRow.h" struct GameConfig; -class BuildingIconCache; -class ItemIconCache; class Simulation; class QPushButton; @@ -47,10 +46,12 @@ class RecipeSelectionDialog : public ModalDialog Q_OBJECT public: - // The icon caches draw each option's recipe line (REQ-UI-ITEM-ICON). Not owned. + // The context's icon caches draw each option's recipe line (REQ-UI-ITEM-ICON), and + // the rest of it lets those items explain themselves (REQ-UI-ITEM-VALUE-TOOLTIP). + // Nothing in it is owned. RecipeSelectionDialog(const std::vector& options, - const QString& title, ItemIconCache* itemIcons, - BuildingIconCache* buildingIcons, QWidget* parent = nullptr); + const QString& title, const ItemTooltipContext& context, + QWidget* parent = nullptr); std::optional getChosenId() const; diff --git a/src/ui/SchematicChoiceDialog.cpp b/src/ui/SchematicChoiceDialog.cpp index 374be5b..4e08601 100644 --- a/src/ui/SchematicChoiceDialog.cpp +++ b/src/ui/SchematicChoiceDialog.cpp @@ -41,8 +41,7 @@ QString grantKindLabel(SchematicType type) SchematicChoiceDialog::SchematicChoiceDialog( const std::vector& options, - const RecipesConfig& recipes, ItemIconCache* itemIcons, - BuildingIconCache* buildingIcons, + const RecipesConfig& recipes, const ItemTooltipContext& context, QWidget* parent) : ModalDialog(parent) , m_chosenIndex(0) @@ -130,8 +129,11 @@ SchematicChoiceDialog::SchematicChoiceDialog( 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). + // that runs it and its name, so the line says everything there is to + // say about the recipe itself (REQ-DEF-SCHEMATIC-DROP). The items it + // names still explain themselves, and the card around them is not + // clicked -- the Select button below it is -- so a click on one says + // it at once (REQ-UI-ITEM-VALUE-TOOLTIP). RecipeLineRow::Spec spec; spec.building = def->building; spec.name = QString::fromStdString(toDisplayName(def->id)); @@ -140,8 +142,10 @@ SchematicChoiceDialog::SchematicChoiceDialog( spec.durationSeconds = def->durationSeconds; RecipeLineRow* line = - new RecipeLineRow(itemIcons, buildingIcons, card); + new RecipeLineRow(context.itemIcons, context.buildingIcons, card); line->setCardChrome(true); + line->setItemTooltips(context, + TooltipTrigger::Trigger::HoverAndClick); cardLayout->addWidget(line); line->setLine(spec); } diff --git a/src/ui/SchematicChoiceDialog.h b/src/ui/SchematicChoiceDialog.h index d6c0b9a..14c4314 100644 --- a/src/ui/SchematicChoiceDialog.h +++ b/src/ui/SchematicChoiceDialog.h @@ -2,12 +2,11 @@ #include +#include "ItemTooltipContext.h" #include "ModalDialog.h" #include "SchematicChoiceOption.h" struct RecipesConfig; -class BuildingIconCache; -class ItemIconCache; // The drop's choice dialog (REQ-DEF-SCHEMATIC-DROP). Unlike every other dialog it cannot // be dismissed: clicking an option is the only way out, so getChosenIndex() always names @@ -19,11 +18,13 @@ class SchematicChoiceDialog : public ModalDialog 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. + // The context's icon caches draw the recipe lines of the "Unlocks recipes" list + // (REQ-DEF-SCHEMATIC-DROP, REQ-UI-ITEM-ICON, REQ-UI-BUILD-ICON), and the rest of it + // lets the items those lines name explain themselves (REQ-UI-ITEM-VALUE-TOOLTIP). + // Nothing in it is owned. SchematicChoiceDialog(const std::vector& options, - const RecipesConfig& recipes, ItemIconCache* itemIcons, - BuildingIconCache* buildingIcons, + const RecipesConfig& recipes, + const ItemTooltipContext& context, QWidget* parent = nullptr); int getChosenIndex() const; diff --git a/src/ui/ShipLayoutDialog.cpp b/src/ui/ShipLayoutDialog.cpp index 8d80549..229c371 100644 --- a/src/ui/ShipLayoutDialog.cpp +++ b/src/ui/ShipLayoutDialog.cpp @@ -418,17 +418,17 @@ private: // ShipLayoutDialog implementation // --------------------------------------------------------------------------- -ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, - const std::string& shipId, +ShipLayoutDialog::ShipLayoutDialog(const std::string& shipId, const ShipLayoutConfig& currentLayout, std::vector& allBlueprints, std::set unlockedModuleIds, bool debugDraw, - ItemIconCache* itemIcons, + const ItemTooltipContext& context, QWidget* parent) : ModalDialog(parent) - , m_config(config) - , m_itemIcons(itemIcons) + , m_context(context) + , m_config(context.config) + , m_itemIcons(context.itemIcons) , m_shipId(shipId) , m_unlockedModuleIds(std::move(unlockedModuleIds)) , m_rows(0) @@ -441,7 +441,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, , m_debugDraw(debugDraw) { // Find the ship's layout grid. - const ShipDef* shipDef = config->ships.findShipDef(shipId); + const ShipDef* shipDef = m_config->ships.findShipDef(shipId); if (shipDef) { m_shipLayout = shipDef->layout; @@ -495,12 +495,15 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, // 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 = new ShipStatsPanel(m_config, this); m_statsPanel->setDebugDrawEnabled(m_debugDraw); leftLayout->addWidget(m_statsPanel); m_buildCostSection = new SectionBox(tr("Build cost"), this); m_buildCostLine = new RecipeLineRow(m_itemIcons, nullptr, m_buildCostSection); + // A section of the dialog rather than a control, so its materials explain themselves + // on hover or click, as they do in the panel (REQ-UI-ITEM-VALUE-TOOLTIP). + m_buildCostLine->setItemTooltips(m_context, TooltipTrigger::Trigger::HoverAndClick); m_buildCostSection->getContentLayout()->addWidget(m_buildCostLine); leftLayout->addWidget(m_buildCostSection); leftLayout->addStretch(1); @@ -517,9 +520,9 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, int row = 0; const int kCols = 2; - for (int i = 0; i < static_cast(config->modules.modules.size()); ++i) + for (int i = 0; i < static_cast(m_config->modules.modules.size()); ++i) { - const ModuleDef& def = config->modules.modules[i]; + const ModuleDef& def = m_config->modules.modules[i]; if (m_unlockedModuleIds.count(def.id) == 0) { m_moduleButtons.push_back(nullptr); @@ -553,7 +556,10 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, // The config tooltip stays: it says what the module does, which the cost line // does not (REQ-MOD-UI-MODULE-TOOLTIP). Hover only: the click selects the module - // (REQ-UI-TOOLTIP-TRIGGER). + // (REQ-UI-TOOLTIP-TRIGGER). The materials on the face stay silent whether or not + // this module fills its description in -- the button is a described control, and + // one module's config gap must not make it read differently from the one beside + // it (REQ-UI-ITEM-VALUE-TOOLTIP). if (def.tooltip) { TooltipTrigger::attachText(*btn, QString::fromStdString(*def.tooltip), diff --git a/src/ui/ShipLayoutDialog.h b/src/ui/ShipLayoutDialog.h index 6bd7835..96cc7d0 100644 --- a/src/ui/ShipLayoutDialog.h +++ b/src/ui/ShipLayoutDialog.h @@ -9,6 +9,7 @@ #include #include "GameConfig.h" +#include "ItemTooltipContext.h" #include "ModalDialog.h" #include "Rotation.h" #include "ShipLayout.h" @@ -25,15 +26,16 @@ class ShipLayoutDialog : public ModalDialog 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, + // The context's item cache draws what each module costs and what the layout costs in + // total (REQ-MOD-UI-DIALOG, REQ-MOD-UI-STATS-PANEL), and the rest of it lets the + // build cost explain the items it names (REQ-UI-ITEM-VALUE-TOOLTIP). Nothing in it is + // owned. + ShipLayoutDialog(const std::string& shipId, const ShipLayoutConfig& currentLayout, std::vector& allBlueprints, std::set unlockedModuleIds, bool debugDraw, - ItemIconCache* itemIcons, + const ItemTooltipContext& context, QWidget* parent = nullptr); std::optional getResult() const; @@ -78,6 +80,7 @@ private: std::vector rotatedMask(const ModuleDef& def, Rotation rotation) const; void loadLayoutBlueprint(const std::vector& modules); + ItemTooltipContext m_context; const GameConfig* m_config; ItemIconCache* m_itemIcons; std::string m_shipId; diff --git a/src/ui/selection/BufferedBuildingContent.cpp b/src/ui/selection/BufferedBuildingContent.cpp index 0ec81b1..5ba6afc 100644 --- a/src/ui/selection/BufferedBuildingContent.cpp +++ b/src/ui/selection/BufferedBuildingContent.cpp @@ -67,6 +67,11 @@ 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 RecipeLineRow(context.itemIcons, context.buildingIcons, this); + // Its icons name items, and nothing in the panel is clicked, so each explains itself + // on hover or click -- which is how the player follows an input back to what makes it + // without that item being in a buffer (REQ-UI-ITEM-VALUE-TOOLTIP). + m_recipeSummary->setItemTooltips(context.getItemTooltipContext(), + TooltipTrigger::Trigger::HoverAndClick); getConfigurationLayout()->addWidget(m_recipeSummary); m_inputSection = new SectionBox(tr("Input buffers"), this); diff --git a/src/ui/selection/CMakeLists.txt b/src/ui/selection/CMakeLists.txt index a9e93f8..28ed646 100644 --- a/src/ui/selection/CMakeLists.txt +++ b/src/ui/selection/CMakeLists.txt @@ -14,7 +14,6 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.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 @@ -48,7 +47,6 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.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 diff --git a/src/ui/selection/DebrisContent.cpp b/src/ui/selection/DebrisContent.cpp index 63ea333..8cc6ee4 100644 --- a/src/ui/selection/DebrisContent.cpp +++ b/src/ui/selection/DebrisContent.cpp @@ -5,6 +5,7 @@ #include "DebrisScrap.h" #include "IconCaption.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" #include "Simulation.h" #include "StatRow.h" @@ -23,6 +24,11 @@ DebrisContent::DebrisContent(const SelectionContext& context, m_scrapIcon.isNull() ? tr("Scrap remaining") : tr("Remaining"), this); m_scrapRow->setValueIcon(m_scrapIcon); m_scrapRow->setValueEmphasized(true); + // Scrap is an item like any other, so the value explains it -- for scrap, that it is + // salvaged rather than made (REQ-UI-ITEM-VALUE-TOOLTIP, REQ-UI-ITEM-TOOLTIP). + ItemTooltip::attachTo(m_scrapRow->getValueWidget(), + context.getItemTooltipContext(), kScrapItemId, + TooltipTrigger::Trigger::HoverAndClick); getRuntimeLayout()->addWidget(m_scrapRow); setIdentity(QPixmap(), tr("Debris")); diff --git a/src/ui/selection/FieldMultiContent.cpp b/src/ui/selection/FieldMultiContent.cpp index 44ca298..900b104 100644 --- a/src/ui/selection/FieldMultiContent.cpp +++ b/src/ui/selection/FieldMultiContent.cpp @@ -12,6 +12,7 @@ #include "FactionComponent.h" #include "IconCaption.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" #include "ShipIdentityComponent.h" #include "Simulation.h" #include "StatRow.h" @@ -94,6 +95,11 @@ void FieldMultiContent::buildSummary(const std::vector& actors) m_scrapRow->setValueIcon(m_scrapIcon); m_scrapRow->setIndented(true); m_scrapRow->setValueEmphasized(true); + // The same value as the debris card's, so it explains its item the same way + // (REQ-UI-ITEM-VALUE-TOOLTIP). + ItemTooltip::attachTo(m_scrapRow->getValueWidget(), + getContext().getItemTooltipContext(), kScrapItemId, + TooltipTrigger::Trigger::HoverAndClick); getRuntimeLayout()->addWidget(m_scrapRow); } } diff --git a/src/ui/selection/ItemChip.cpp b/src/ui/selection/ItemChip.cpp index 86fd9af..e45969c 100644 --- a/src/ui/selection/ItemChip.cpp +++ b/src/ui/selection/ItemChip.cpp @@ -73,8 +73,8 @@ ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId, // A chip displays a quantity and does nothing when clicked, so a click brings its // tooltip up at once rather than waiting the hover out (REQ-UI-TOOLTIP-TRIGGER). - ItemTooltip* tooltip = new ItemTooltip(context, itemId, this); - TooltipTrigger::attach(*this, *tooltip, TooltipTrigger::Trigger::HoverAndClick); + ItemTooltip::attachTo(*this, context.getItemTooltipContext(), itemId, + TooltipTrigger::Trigger::HoverAndClick); } void ItemChip::setCount(const QString& count) diff --git a/src/ui/selection/ItemTooltip.h b/src/ui/selection/ItemTooltip.h deleted file mode 100644 index 0492f87..0000000 --- a/src/ui/selection/ItemTooltip.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include - -#include "SelectionContext.h" -#include "Tooltip.h" - -// The tooltip an item chip shows: the item's name, and every way the player can currently -// produce it, each drawn as a recipe line (REQ-UI-ITEM-TOOLTIP). -// -// Its content is drawn -- item squares, building chips and all -- rather than written, -// which is what it adds to the plain text tooltip it derives from. Rebuilt on every show, -// because what produces an item changes as the player unlocks recipes -// (REQ-LOCK-UI-RECIPE). -class ItemTooltip : public Tooltip -{ - Q_OBJECT - -public: - ItemTooltip(const SelectionContext& context, const std::string& itemId, - QWidget* parent = nullptr); - -protected: - void refreshContent() override; - -private: - SelectionContext m_context; - std::string m_itemId; -}; diff --git a/src/ui/selection/MultiBuildingContent.cpp b/src/ui/selection/MultiBuildingContent.cpp index d905be0..5a0d5fe 100644 --- a/src/ui/selection/MultiBuildingContent.cpp +++ b/src/ui/selection/MultiBuildingContent.cpp @@ -12,6 +12,7 @@ #include "GameConfig.h" #include "IconCaption.h" #include "ItemIconCache.h" +#include "ItemTooltip.h" #include "SelectionNames.h" #include "Simulation.h" #include "StatRow.h" @@ -103,5 +104,10 @@ void MultiBuildingContent::buildSummary() getContext().itemIcons->getInlineIcon(kBlockItemId, font())); totalRow->setValue(QString::number(totalCost)); totalRow->setValueEmphasized(true); + // The total names an item, so it explains it, and nothing here is clickable, so a + // click says it at once (REQ-UI-ITEM-VALUE-TOOLTIP). + ItemTooltip::attachTo(totalRow->getValueWidget(), + getContext().getItemTooltipContext(), kBlockItemId, + TooltipTrigger::Trigger::HoverAndClick); getRuntimeLayout()->addWidget(totalRow); } diff --git a/src/ui/selection/SelectionContext.h b/src/ui/selection/SelectionContext.h index 4414016..28e0419 100644 --- a/src/ui/selection/SelectionContext.h +++ b/src/ui/selection/SelectionContext.h @@ -1,5 +1,7 @@ #pragma once +#include "ItemTooltipContext.h" + struct GameConfig; struct VisualsConfig; class BuildingIconCache; @@ -26,4 +28,11 @@ struct SelectionContext // because cards outlive a toggle: the panel owns the flag, so a card reading it per // refresh always sees the current value without subscribing to the event itself. const bool* debugDrawEnabled = nullptr; + + // The part of this a tooltip needs, for the panel's item displays to explain what + // they name (REQ-UI-ITEM-VALUE-TOOLTIP). + ItemTooltipContext getItemTooltipContext() const + { + return ItemTooltipContext{ sim, config, itemIcons, buildingIcons }; + } }; diff --git a/src/ui/selection/StatRow.cpp b/src/ui/selection/StatRow.cpp index 3da8c59..6037957 100644 --- a/src/ui/selection/StatRow.cpp +++ b/src/ui/selection/StatRow.cpp @@ -65,6 +65,11 @@ void StatRow::setIndented(bool indented) layout()->setContentsMargins(indented ? kIndentPx : 0, 0, 0, 0); } +QWidget& StatRow::getValueWidget() +{ + return *m_valueLabel; +} + void StatRow::updateValue() { if (m_valueIcon.isNull()) diff --git a/src/ui/selection/StatRow.h b/src/ui/selection/StatRow.h index 652ccf0..abd32f1 100644 --- a/src/ui/selection/StatRow.h +++ b/src/ui/selection/StatRow.h @@ -34,6 +34,11 @@ public: // Indents the row, so it reads as belonging to the row above it -- the scrap total // under a debris count (REQ-UI-FIELD-MULTI-SELECTION). void setIndented(bool indented); + // The widget the value is drawn in, number and item icon together, for a caller that + // gives it the tooltip of the item it names (REQ-UI-ITEM-VALUE-TOOLTIP). Exposed + // rather than attached here: this row knows nothing of the simulation, which is what + // lets it serve the balancing tool as well as the panel. + QWidget& getValueWidget(); private: // Re-states the value in whichever form it currently takes. Needed because the icon