Files
dota_factory/src/ui/HeaderBar.cpp
Malte Langkabel 44ec3c51f1 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-18 21:41:34 +02:00

218 lines
7.4 KiB
C++

#include "HeaderBar.h"
#include <cmath>
#include <string>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QPalette>
#include <QPushButton>
#include <QSignalMapper>
#include <QSize>
#include "Command.h"
#include "CommandRequestedEvent.h"
#include "EventManager.h"
#include "IconCaption.h"
#include "ItemIconCache.h"
#include "ItemTooltip.h"
#include "Simulation.h"
#include "SpeedChangeRequestedEvent.h"
#include "Tick.h"
#include "TooltipTrigger.h"
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int HeaderBar::kSpeedCount = 5;
HeaderBar::HeaderBar(const ItemTooltipContext& context, QWidget* parent)
: QWidget(parent)
, m_context(context)
{
QHBoxLayout* layout = new QHBoxLayout(this);
layout->setContentsMargins(8, 4, 8, 4);
layout->setSpacing(8);
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);
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 (m_context.config->world.artifactTooltip)
{
TooltipTrigger::attachText(
*m_artifactsLabel,
QString::fromStdString(*m_context.config->world.artifactTooltip),
TooltipTrigger::Trigger::HoverAndClick);
}
m_bossWaveLabel = new QLabel(tr("Boss Wave #1"), this);
m_nextBossLabel = new QLabel(tr("Next boss: 5:00"), this);
// Asteroid expansion button, to the left of the speed buttons (REQ-UI-HEADER,
// REQ-UI-EXPAND-BUTTON). Caption/enabled state are set on the first
// ExpansionCostChangedEvent; clicking requests an ExpandAsteroidCommand.
m_expandButton = new QPushButton(tr("Expand"), this);
connect(m_expandButton, &QPushButton::clicked, this, []() {
EventManager::getInstance()->sendEventImmediately(
std::make_shared<CommandRequestedEvent>(
std::make_shared<ExpandAsteroidCommand>()));
});
const int itemSpacing = 20;
layout->addWidget(m_timeLabel);
layout->addSpacing(itemSpacing);
layout->addWidget(m_blocksLabel);
layout->addSpacing(itemSpacing);
layout->addWidget(m_artifactsLabel);
layout->addStretch();
layout->addWidget(m_bossWaveLabel);
layout->addSpacing(itemSpacing);
layout->addWidget(m_nextBossLabel);
layout->addSpacing(itemSpacing);
layout->addWidget(m_expandButton);
layout->addSpacing(itemSpacing);
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
QSignalMapper* mapper = new QSignalMapper(this);
for (int i = 0; i < kSpeedCount; ++i)
{
QPushButton* btn = new QPushButton(labels[i], this);
btn->setCheckable(true);
btn->setChecked(i == 2);
layout->addWidget(btn);
m_speedButtons.push_back(btn);
mapper->setMapping(btn, i);
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
}
connect(mapper, qOverload<int>(&QSignalMapper::mapped), this, &HeaderBar::onSpeedButton);
setFixedHeight(sizeHint().height());
registerForEvents();
}
HeaderBar::~HeaderBar()
{
unregisterForEvents();
}
void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{
const int totalSeconds = static_cast<int>(ticksToSeconds(m_context.sim->getCurrentTick()));
m_timeLabel->setText(
QString("%1:%2")
.arg(totalSeconds / 60, 2, 10, QChar('0'))
.arg(totalSeconds % 60, 2, 10, QChar('0')));
}
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
{
updateBlocksLabel();
updateExpandButton();
}
void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> /*event*/)
{
updateExpandButton();
}
void HeaderBar::updateBlocksLabel()
{
const int blocks = m_context.sim->getBuildingBlocksStock();
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).
m_blocksLabel->setText(tr("Stock: %1 Blocks").arg(blocks));
return;
}
m_blocksLabel->setPixmap(renderCaptionWithIcon(
tr("Stock: %1").arg(blocks), icon, font(),
m_blocksLabel->palette().color(QPalette::WindowText)));
}
void HeaderBar::updateExpandButton()
{
const int blocks = m_context.sim->getBuildingBlocksStock();
const int expansionCost = m_context.sim->getCurrentExpansionCost();
m_expandButton->setEnabled(blocks >= expansionCost);
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).
m_expandButton->setIcon(QIcon());
m_expandButton->setText(tr("Expand: %1 Blocks").arg(expansionCost));
return;
}
const QString text = tr("Expand: %1").arg(expansionCost);
const QPalette& pal = m_expandButton->palette();
const QPixmap normal = renderCaptionWithIcon(
text, icon, m_expandButton->font(), pal.color(QPalette::ButtonText));
const QPixmap greyed = renderCaptionWithIcon(
text, icon, m_expandButton->font(),
pal.color(QPalette::Disabled, QPalette::ButtonText));
QIcon buttonIcon;
buttonIcon.addPixmap(normal, QIcon::Normal);
buttonIcon.addPixmap(greyed, QIcon::Disabled);
m_expandButton->setText(QString());
m_expandButton->setIcon(buttonIcon);
const qreal dpr = normal.devicePixelRatio();
m_expandButton->setIconSize(QSize(
static_cast<int>(normal.width() / dpr),
static_cast<int>(normal.height() / dpr)));
}
void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
{
for (int i = 0; i < kSpeedCount; ++i)
{
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(
std::abs(kSpeeds[i] - event->speed) < 0.001);
}
}
void HeaderBar::handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> /*event*/)
{
const Tick countdownTicks = m_context.sim->getBossCountdownTicks();
const int bossSeconds = static_cast<int>(
ticksToSeconds(countdownTicks > 0 ? countdownTicks : 0));
m_bossWaveLabel->setText(
tr("Boss Wave #%1")
.arg(m_context.sim->getBossWaveCounter()));
m_nextBossLabel->setText(
tr("Next boss: %1:%2")
.arg(bossSeconds / 60)
.arg(bossSeconds % 60, 2, 10, QChar('0')));
}
void HeaderBar::handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> /*event*/)
{
m_artifactsLabel->setText(
tr("Artifacts: %1/%2")
.arg(m_context.sim->getArtifactCount())
.arg(m_context.sim->getConfig().world.artifacts.artifactWinCount));
}
void HeaderBar::onSpeedButton(int index)
{
if (index >= 0 && index < kSpeedCount)
{
EventManager::getInstance()->sendEventImmediately(
std::make_shared<SpeedChangeRequestedEvent>(kSpeeds[index]));
}
}