show a tooltip on click, and keep it up until the pointer leaves
One tooltip mechanism for the whole UI: a Tooltip popup plus a TooltipTrigger event filter, replacing every setToolTip call. Qt's own tooltip cannot do what REQ-UI-TOOLTIP-TRIGGER and REQ-UI-TOOLTIP-DISMISS ask for -- it times out, hides on the first mouse move, cannot be hovered and cannot be brought up by a click. A tooltip now appears the moment an element with no click action of its own is clicked, and it is placed with its top-left corner on the pointer, so the pointer can move onto the tooltip and hold it open. The pointer is polled while a tooltip is up rather than tracked through enter and leave events, whose order depends on which widget the pointer crosses first. The item chip's children become transparent to the mouse. They were taking the chip's enter and leave events, so its tooltip only ever appeared when the pointer rested on the chip's padding. The modal header's close button loses its "Close" tooltip: a close button in a header says that by being one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -439,10 +439,10 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
|
|
||||||
### Tooltips
|
### Tooltips
|
||||||
|
|
||||||
Several UI elements carry a tooltip: the header bar's building blocks stock display (REQ-UI-BLOCKS-TOOLTIP) and artifact count (REQ-UI-ARTIFACTS-TOOLTIP), the selection panel's item chips (REQ-UI-ITEM-TOOLTIP), the build button bar's building-type buttons (REQ-UI-BUILD-TOOLTIP) and Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON), and the layout configuration dialog's module selection buttons (REQ-MOD-UI-MODULE-TOOLTIP). Each of those requirements defines what its tooltip says; the two requirements here define how every one of them is shown and hidden.
|
Several UI elements carry a tooltip: the header bar's building blocks stock display (REQ-UI-BLOCKS-TOOLTIP) and artifact count (REQ-UI-ARTIFACTS-TOOLTIP), the selection panel's item chips (REQ-UI-ITEM-TOOLTIP), the build button bar's building-type buttons (REQ-UI-BUILD-TOOLTIP) and Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON), the layout configuration dialog's module selection buttons (REQ-MOD-UI-MODULE-TOOLTIP), and the blueprint card's delete icon (REQ-UI-BLUEPRINT-DELETE). Each of those requirements defines what its tooltip says; the two requirements here define how every one of them is shown and hidden. An element not named there carries no tooltip at all: the modal header's close button (REQ-UI-MODAL-CHROME) says what it does by being a close button, and the selection dialog's option buttons describe themselves on their own faces (REQ-UI-SELECT-OPTIONS).
|
||||||
|
|
||||||
- REQ-UI-TOOLTIP-TRIGGER: A tooltip is shown by **hovering** the element it belongs to, after the usual short hover delay. On an element that has **no click action of its own** it is in addition shown **immediately on left click**, without waiting the delay out. That covers the header bar's building blocks stock display and artifact count and the selection panel's item chips: these display a value and do nothing when clicked, so a click on them is free to mean "tell me what this is", which is quicker than waiting on a hover and is the obvious thing to try. It does not cover elements whose click already does something — the building-type buttons, the Deconstruct button, and the module selection buttons — whose tooltips are reached by hovering alone, so that one gesture never both acts and explains. Only the left mouse button shows a tooltip; the right and middle buttons never do. Clicking an element whose tooltip is already up leaves it up.
|
- REQ-UI-TOOLTIP-TRIGGER: A tooltip is shown by **hovering** the element it belongs to, after the usual short hover delay. On an element that has **no click action of its own** it is in addition shown **immediately on left click**, without waiting the delay out. That covers the header bar's building blocks stock display and artifact count and the selection panel's item chips: these display a value and do nothing when clicked, so a click on them is free to mean "tell me what this is", which is quicker than waiting on a hover and is the obvious thing to try. It does not cover elements whose click already does something — the building-type buttons, the Deconstruct button, and the module selection buttons — whose tooltips are reached by hovering alone, so that one gesture never both acts and explains. Only the left mouse button shows a tooltip; the right and middle buttons never do. Clicking an element whose tooltip is already up leaves it up.
|
||||||
- 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 requires the tooltip to be placed **adjacent to its element**, leaving no gap the pointer would have to cross to reach it. 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-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.
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
@@ -838,7 +838,7 @@ Blueprints occupy no permanent screen space. They are saved with **Ctrl+C** from
|
|||||||
- After the transfer the game stays in blueprint placement mode, so further same-type buildings can be clicked in turn.
|
- After the transfer the game stays in blueprint placement mode, so further same-type buildings can be clicked in turn.
|
||||||
- A blueprint placement applies every transfer among its ghosts in the same click that places its new construction sites (REQ-UI-BLUEPRINT-PLACE); the placement is all-or-nothing, so if any ghost is invalid nothing is placed and nothing is transferred.
|
- A blueprint placement applies every transfer among its ghosts in the same click that places its new construction sites (REQ-UI-BLUEPRINT-PLACE); the placement is all-or-nothing, so if any ghost is invalid nothing is placed and nothing is transferred.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint card (REQ-UI-BLUEPRINT-CARD) immediately removes that blueprint from the list, without a confirmation prompt. The blueprint selection dialog stays open and its card grid reflows to close the gap. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint card (REQ-UI-BLUEPRINT-CARD) immediately removes that blueprint from the list, without a confirmation prompt. The icon carries a hover tooltip naming what it does, since a bare "×" on a card could as easily mean closing something (REQ-UI-TOOLTIP-TRIGGER, REQ-UI-TOOLTIP-DISMISS). The blueprint selection dialog stays open and its card grid reflows to close the gap. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
|
- REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include "BlueprintLibrary.h"
|
#include "BlueprintLibrary.h"
|
||||||
#include "IconCaption.h"
|
#include "IconCaption.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -268,7 +269,9 @@ void BlueprintSelectionDialog::rebuildGrid()
|
|||||||
// must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE).
|
// must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE).
|
||||||
QPushButton* deleteButton = new QPushButton(QString(kCrossGlyph), card);
|
QPushButton* deleteButton = new QPushButton(QString(kCrossGlyph), card);
|
||||||
deleteButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
deleteButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||||
deleteButton->setToolTip(tr("Delete blueprint"));
|
// Hover only: the click deletes the blueprint (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
TooltipTrigger::attachText(*deleteButton, tr("Delete blueprint"),
|
||||||
|
TooltipTrigger::Trigger::HoverOnly);
|
||||||
deleteButton->move(cardSize.width() - kSmallButtonSizePx - kCardPaddingPx / 2,
|
deleteButton->move(cardSize.width() - kSmallButtonSizePx - kCardPaddingPx / 2,
|
||||||
cardSize.height() - kSmallButtonSizePx - kCardPaddingPx / 2);
|
cardSize.height() - kSmallButtonSizePx - kCardPaddingPx / 2);
|
||||||
deleteButton->raise();
|
deleteButton->raise();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "InputMapper.h"
|
#include "InputMapper.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -211,10 +212,12 @@ BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
|||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
// The button face carries no name (REQ-UI-BUILD-COST), so the tooltip always
|
// The button face carries no name (REQ-UI-BUILD-COST), so the tooltip always
|
||||||
// leads with it and adds the config description when there is one
|
// leads with it and adds the config description when there is one
|
||||||
// (REQ-UI-BUILD-TOOLTIP).
|
// (REQ-UI-BUILD-TOOLTIP). Hover only: the click enters builder mode
|
||||||
btn->setToolTip(def.tooltip
|
// (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
TooltipTrigger::attachText(*btn, def.tooltip
|
||||||
? QStringLiteral("%1\n%2").arg(name, QString::fromStdString(*def.tooltip))
|
? QStringLiteral("%1\n%2").arg(name, QString::fromStdString(*def.tooltip))
|
||||||
: name);
|
: name,
|
||||||
|
TooltipTrigger::Trigger::HoverOnly);
|
||||||
layout->addWidget(btn);
|
layout->addWidget(btn);
|
||||||
|
|
||||||
const int idx = static_cast<int>(m_buttons.size());
|
const int idx = static_cast<int>(m_buttons.size());
|
||||||
@@ -253,7 +256,9 @@ BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
|||||||
"block cost once removed; a construction site removed before it is built "
|
"block cost once removed; a construction site removed before it is built "
|
||||||
"is refunded in full.")
|
"is refunded in full.")
|
||||||
.arg(refundPercentage);
|
.arg(refundPercentage);
|
||||||
m_deconstructButton->setToolTip(deconstructTooltip);
|
// Hover only: the click toggles the mode (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
TooltipTrigger::attachText(*m_deconstructButton, deconstructTooltip,
|
||||||
|
TooltipTrigger::Trigger::HoverOnly);
|
||||||
layout->addWidget(m_deconstructButton);
|
layout->addWidget(m_deconstructButton);
|
||||||
connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
|
connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/Tooltip.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TooltipTrigger.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.h
|
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||||
@@ -68,6 +70,8 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/Tooltip.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TooltipTrigger.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
const int HeaderBar::kSpeedCount = 5;
|
const int HeaderBar::kSpeedCount = 5;
|
||||||
@@ -34,18 +35,24 @@ HeaderBar::HeaderBar(const Simulation* sim, const GameConfig* config,
|
|||||||
layout->setSpacing(8);
|
layout->setSpacing(8);
|
||||||
|
|
||||||
m_timeLabel = new QLabel("00:00", this);
|
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).
|
||||||
m_blocksLabel = new QLabel(this);
|
m_blocksLabel = new QLabel(this);
|
||||||
if (config->world.buildingBlocksTooltip)
|
if (config->world.buildingBlocksTooltip)
|
||||||
{
|
{
|
||||||
m_blocksLabel->setToolTip(
|
TooltipTrigger::attachText(
|
||||||
QString::fromStdString(*config->world.buildingBlocksTooltip));
|
*m_blocksLabel,
|
||||||
|
QString::fromStdString(*config->world.buildingBlocksTooltip),
|
||||||
|
TooltipTrigger::Trigger::HoverAndClick);
|
||||||
}
|
}
|
||||||
updateBlocksLabel();
|
updateBlocksLabel();
|
||||||
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
||||||
if (config->world.artifactTooltip)
|
if (config->world.artifactTooltip)
|
||||||
{
|
{
|
||||||
m_artifactsLabel->setToolTip(
|
TooltipTrigger::attachText(
|
||||||
QString::fromStdString(*config->world.artifactTooltip));
|
*m_artifactsLabel,
|
||||||
|
QString::fromStdString(*config->world.artifactTooltip),
|
||||||
|
TooltipTrigger::Trigger::HoverAndClick);
|
||||||
}
|
}
|
||||||
m_bossWaveLabel = new QLabel(tr("Boss Wave #1"), this);
|
m_bossWaveLabel = new QLabel(tr("Boss Wave #1"), this);
|
||||||
m_nextBossLabel = new QLabel(tr("Next boss: 5:00"), this);
|
m_nextBossLabel = new QLabel(tr("Next boss: 5:00"), this);
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ QHBoxLayout* ModalDialog::addHeader(QBoxLayout* mainLayout, const QString& title
|
|||||||
{
|
{
|
||||||
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
||||||
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||||
closeButton->setToolTip(tr("Close"));
|
|
||||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
headerLayout->addWidget(closeButton);
|
headerLayout->addWidget(closeButton);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "RecipeLineRow.h"
|
#include "RecipeLineRow.h"
|
||||||
#include "SectionBox.h"
|
#include "SectionBox.h"
|
||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -551,10 +552,12 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
costLine->setLine(cost);
|
costLine->setLine(cost);
|
||||||
|
|
||||||
// The config tooltip stays: it says what the module does, which the cost line
|
// The config tooltip stays: it says what the module does, which the cost line
|
||||||
// does not (REQ-MOD-UI-MODULE-TOOLTIP).
|
// does not (REQ-MOD-UI-MODULE-TOOLTIP). Hover only: the click selects the module
|
||||||
|
// (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
if (def.tooltip)
|
if (def.tooltip)
|
||||||
{
|
{
|
||||||
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
TooltipTrigger::attachText(*btn, QString::fromStdString(*def.tooltip),
|
||||||
|
TooltipTrigger::Trigger::HoverOnly);
|
||||||
}
|
}
|
||||||
buttonGrid->addWidget(btn, row, col);
|
buttonGrid->addWidget(btn, row, col);
|
||||||
m_moduleButtons.push_back(btn);
|
m_moduleButtons.push_back(btn);
|
||||||
|
|||||||
176
src/ui/Tooltip.cpp
Normal file
176
src/ui/Tooltip.cpp
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
#include "Tooltip.h"
|
||||||
|
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayoutItem>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Widest a text tooltip gets before it wraps, in device-independent pixels. A config
|
||||||
|
// description runs to a sentence or two and would otherwise be laid out as one line
|
||||||
|
// across the whole window.
|
||||||
|
const int kMaxTextWidthPx = 360;
|
||||||
|
|
||||||
|
// Height the wrap is measured against: high enough that it never cuts the text short.
|
||||||
|
const int kMeasureHeightPx = 10000;
|
||||||
|
|
||||||
|
|
||||||
|
// The screen the given position is on, falling back to the primary screen when it is on
|
||||||
|
// none of them (a position 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
|
||||||
|
|
||||||
|
|
||||||
|
Tooltip* Tooltip::s_visibleTooltip = nullptr;
|
||||||
|
|
||||||
|
Tooltip* Tooltip::getVisibleTooltip()
|
||||||
|
{
|
||||||
|
return s_visibleTooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::hideVisibleTooltip()
|
||||||
|
{
|
||||||
|
if (s_visibleTooltip != nullptr)
|
||||||
|
{
|
||||||
|
s_visibleTooltip->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tooltip::Tooltip(QWidget* parent)
|
||||||
|
: QFrame(parent, Qt::ToolTip)
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tooltip::~Tooltip()
|
||||||
|
{
|
||||||
|
// Destroyed while on screen -- a chip whose panel is rebuilt takes its tooltip with
|
||||||
|
// it (REQ-UI-SINGLE-SELECTION).
|
||||||
|
if (s_visibleTooltip == this)
|
||||||
|
{
|
||||||
|
s_visibleTooltip = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::setText(const QString& text)
|
||||||
|
{
|
||||||
|
clearContent();
|
||||||
|
|
||||||
|
QLabel* label = new QLabel(text, this);
|
||||||
|
// Plain text throughout: config strings are read as they are written, never as
|
||||||
|
// markup Qt happens to recognize.
|
||||||
|
label->setTextFormat(Qt::PlainText);
|
||||||
|
label->setWordWrap(true);
|
||||||
|
|
||||||
|
// The width the text needs, capped at the wrap measure. Measured rather than left to
|
||||||
|
// the layout, so the label is laid out at exactly the width its wrapping assumed and
|
||||||
|
// the tooltip ends up as tall as the wrapped text really is.
|
||||||
|
const QFontMetrics metrics(label->font());
|
||||||
|
const QRect wrapped = metrics.boundingRect(
|
||||||
|
QRect(0, 0, kMaxTextWidthPx, kMeasureHeightPx), Qt::TextWordWrap, text);
|
||||||
|
label->setFixedWidth(qMin(kMaxTextWidthPx, wrapped.width()));
|
||||||
|
// The height that wrapping really needs, so a label whose own hint was computed for
|
||||||
|
// the unwrapped text cannot leave the last lines clipped.
|
||||||
|
label->setMinimumHeight(wrapped.height());
|
||||||
|
|
||||||
|
m_layout->addWidget(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::showAt(const QPoint& globalPos)
|
||||||
|
{
|
||||||
|
// Only ever one tooltip on screen (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
if (s_visibleTooltip != nullptr && s_visibleTooltip != this)
|
||||||
|
{
|
||||||
|
s_visibleTooltip->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshContent();
|
||||||
|
adjustSize();
|
||||||
|
|
||||||
|
// The top-left corner goes on the pointer, so the pointer starts on the tooltip and
|
||||||
|
// reaches it without crossing a gap (REQ-UI-TOOLTIP-DISMISS). Kept on the screen the
|
||||||
|
// pointer is on: a tooltip near the right or bottom edge would otherwise be cut off.
|
||||||
|
// Clamping keeps the pointer inside the tooltip, since it only ever moves the
|
||||||
|
// tooltip back over the pointer.
|
||||||
|
const QRect screen = availableScreenRect(globalPos);
|
||||||
|
QPoint topLeft = globalPos;
|
||||||
|
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();
|
||||||
|
raise();
|
||||||
|
s_visibleTooltip = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tooltip::getContainsGlobalPos(const QPoint& globalPos) const
|
||||||
|
{
|
||||||
|
return isVisible() && frameGeometry().contains(globalPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::refreshContent()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVBoxLayout* Tooltip::getContentLayout()
|
||||||
|
{
|
||||||
|
return m_layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::clearContent()
|
||||||
|
{
|
||||||
|
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||||
|
{
|
||||||
|
if (item->widget() != nullptr)
|
||||||
|
{
|
||||||
|
// Out of the widget tree at once and destroyed later: a child merely taken
|
||||||
|
// out of the layout would keep its geometry and show through the content
|
||||||
|
// that replaces it.
|
||||||
|
item->widget()->setParent(nullptr);
|
||||||
|
item->widget()->deleteLater();
|
||||||
|
}
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tooltip::hideEvent(QHideEvent* event)
|
||||||
|
{
|
||||||
|
if (s_visibleTooltip == this)
|
||||||
|
{
|
||||||
|
s_visibleTooltip = nullptr;
|
||||||
|
}
|
||||||
|
QFrame::hideEvent(event);
|
||||||
|
}
|
||||||
51
src/ui/Tooltip.h
Normal file
51
src/ui/Tooltip.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QHideEvent;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// The popup every tooltip in the UI is drawn in (REQ-UI-TOOLTIP-TRIGGER,
|
||||||
|
// REQ-UI-TOOLTIP-DISMISS). Shown and hidden by a TooltipTrigger attached to the element
|
||||||
|
// it describes; it carries no trigger logic of its own.
|
||||||
|
//
|
||||||
|
// A window of its own rather than Qt's own tooltip, because ours must not time out, must
|
||||||
|
// stay up while the pointer is on it, and -- for the item production tooltip
|
||||||
|
// (REQ-UI-ITEM-TOOLTIP) -- must hold drawn content rather than text. A Qt::ToolTip window
|
||||||
|
// so it floats above the game window instead of being clipped by it.
|
||||||
|
class Tooltip : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// The tooltip currently on screen, if any: only ever one, since the pointer can only
|
||||||
|
// be on one element.
|
||||||
|
static Tooltip* getVisibleTooltip();
|
||||||
|
static void hideVisibleTooltip();
|
||||||
|
|
||||||
|
explicit Tooltip(QWidget* parent = nullptr);
|
||||||
|
~Tooltip() override;
|
||||||
|
|
||||||
|
// Plain text content, wrapped at a readable measure. Newlines are kept.
|
||||||
|
void setText(const QString& text);
|
||||||
|
// Refreshes the content and shows the tooltip with its top-left corner on the given
|
||||||
|
// screen position, kept inside the screen (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
void showAt(const QPoint& globalPos);
|
||||||
|
bool getContainsGlobalPos(const QPoint& globalPos) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Called before every show, for content that goes stale between them. The base
|
||||||
|
// tooltip's text does not, so this does nothing here.
|
||||||
|
virtual void refreshContent();
|
||||||
|
QVBoxLayout* getContentLayout();
|
||||||
|
void clearContent();
|
||||||
|
|
||||||
|
void hideEvent(QHideEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static Tooltip* s_visibleTooltip;
|
||||||
|
|
||||||
|
QVBoxLayout* m_layout;
|
||||||
|
};
|
||||||
170
src/ui/TooltipTrigger.cpp
Normal file
170
src/ui/TooltipTrigger.cpp
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "Tooltip.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// How long the pointer rests on an element before its tooltip appears, in milliseconds.
|
||||||
|
// One delay for every tooltip in the UI, now that none of them is Qt's (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
const int kHoverDelayMs = 500;
|
||||||
|
|
||||||
|
// How often the pointer is tested against the element and the tooltip while the tooltip
|
||||||
|
// is up, in milliseconds. Short enough to read as immediate, and it runs only then.
|
||||||
|
const int kPointerPollMs = 100;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
void TooltipTrigger::attachText(QWidget& target, const QString& text, Trigger trigger)
|
||||||
|
{
|
||||||
|
Tooltip* tooltip = new Tooltip(&target);
|
||||||
|
tooltip->setText(text);
|
||||||
|
attach(target, *tooltip, trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipTrigger::attach(QWidget& target, Tooltip& tooltip, Trigger trigger)
|
||||||
|
{
|
||||||
|
// Parented to the element, so both die with it.
|
||||||
|
new TooltipTrigger(target, tooltip, trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TooltipTrigger::eventFilter(QObject* watched, QEvent* event)
|
||||||
|
{
|
||||||
|
if (watched != m_target)
|
||||||
|
{
|
||||||
|
return QObject::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->type())
|
||||||
|
{
|
||||||
|
case QEvent::Enter:
|
||||||
|
{
|
||||||
|
m_hoverTimer->start();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QEvent::Leave:
|
||||||
|
{
|
||||||
|
m_hoverTimer->stop();
|
||||||
|
// Not a dismissal in itself: the pointer may be on its way onto the tooltip
|
||||||
|
// (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
hideUnlessPointerHeld();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case QEvent::MouseButtonPress:
|
||||||
|
{
|
||||||
|
const QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
||||||
|
if (m_trigger != Trigger::HoverAndClick
|
||||||
|
|| mouseEvent->button() != Qt::LeftButton)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_hoverTimer->stop();
|
||||||
|
// A click on an element whose tooltip is already up leaves it where it is
|
||||||
|
// (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
if (!m_tooltip->isVisible())
|
||||||
|
{
|
||||||
|
showTooltip();
|
||||||
|
}
|
||||||
|
// Swallowed: the element does nothing with a click, and the panel beneath it
|
||||||
|
// must not take it for one of its own (REQ-UI-SELECTION-PANEL-DRAG).
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case QEvent::Hide:
|
||||||
|
{
|
||||||
|
m_hoverTimer->stop();
|
||||||
|
hideTooltip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QObject::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
TooltipTrigger::TooltipTrigger(QWidget& target, Tooltip& tooltip, Trigger trigger)
|
||||||
|
: QObject(&target)
|
||||||
|
, m_target(&target)
|
||||||
|
, m_tooltip(&tooltip)
|
||||||
|
, m_trigger(trigger)
|
||||||
|
{
|
||||||
|
m_hoverTimer = new QTimer(this);
|
||||||
|
m_hoverTimer->setSingleShot(true);
|
||||||
|
m_hoverTimer->setInterval(kHoverDelayMs);
|
||||||
|
connect(m_hoverTimer, &QTimer::timeout, this, &TooltipTrigger::showTooltip);
|
||||||
|
|
||||||
|
m_pointerTimer = new QTimer(this);
|
||||||
|
m_pointerTimer->setInterval(kPointerPollMs);
|
||||||
|
connect(m_pointerTimer, &QTimer::timeout,
|
||||||
|
this, &TooltipTrigger::hideUnlessPointerHeld);
|
||||||
|
|
||||||
|
m_target->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect TooltipTrigger::getTargetGlobalRect() const
|
||||||
|
{
|
||||||
|
return QRect(m_target->mapToGlobal(QPoint(0, 0)), m_target->size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipTrigger::showTooltip()
|
||||||
|
{
|
||||||
|
// Already up: left where it is rather than moved to wherever the pointer has got to,
|
||||||
|
// which is what a pointer wandering from the tooltip back onto the element would
|
||||||
|
// otherwise do (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
if (!m_target->isVisible() || m_tooltip->isVisible())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_tooltip->showAt(QCursor::pos());
|
||||||
|
m_pointerTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipTrigger::hideTooltip()
|
||||||
|
{
|
||||||
|
m_pointerTimer->stop();
|
||||||
|
m_tooltip->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TooltipTrigger::hideUnlessPointerHeld()
|
||||||
|
{
|
||||||
|
if (!m_tooltip->isVisible())
|
||||||
|
{
|
||||||
|
m_pointerTimer->stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gone with the element it explains. An element that merely moves -- a header label
|
||||||
|
// resized by the number it carries -- is not a dismissal: the pointer is still on the
|
||||||
|
// tooltip, and nothing but the pointer dismisses one (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
if (!m_target->isVisible())
|
||||||
|
{
|
||||||
|
hideTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gone with the game window: a tooltip is a topmost window and would otherwise stay
|
||||||
|
// over whatever the player switched to.
|
||||||
|
if (QApplication::activeWindow() == nullptr)
|
||||||
|
{
|
||||||
|
hideTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint pointer = QCursor::pos();
|
||||||
|
if (!getTargetGlobalRect().contains(pointer)
|
||||||
|
&& !m_tooltip->getContainsGlobalPos(pointer))
|
||||||
|
{
|
||||||
|
hideTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/ui/TooltipTrigger.h
Normal file
61
src/ui/TooltipTrigger.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QEvent;
|
||||||
|
class QTimer;
|
||||||
|
class QWidget;
|
||||||
|
class Tooltip;
|
||||||
|
|
||||||
|
// Shows and hides one element's tooltip (REQ-UI-TOOLTIP-TRIGGER, REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
// Attached to the element as an event filter, so a plain QLabel or QPushButton needs no
|
||||||
|
// subclass of its own to carry a tooltip.
|
||||||
|
//
|
||||||
|
// It replaces QWidget::setToolTip throughout the UI: Qt's own tooltip times out, hides on
|
||||||
|
// the first mouse move, cannot be hovered, and cannot be brought up by a click.
|
||||||
|
class TooltipTrigger : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// What brings the tooltip up. An element whose click already does something keeps to
|
||||||
|
// HoverOnly, so one gesture never both acts and explains (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
enum class Trigger
|
||||||
|
{
|
||||||
|
HoverOnly,
|
||||||
|
HoverAndClick
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attaches a text tooltip, created and owned along with the trigger by the target.
|
||||||
|
static void attachText(QWidget& target, const QString& text, Trigger trigger);
|
||||||
|
// Attaches a tooltip built by the caller -- the item production tooltip, whose
|
||||||
|
// content is drawn rather than written (REQ-UI-ITEM-TOOLTIP).
|
||||||
|
static void attach(QWidget& target, Tooltip& tooltip, Trigger trigger);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TooltipTrigger(QWidget& target, Tooltip& tooltip, Trigger trigger);
|
||||||
|
|
||||||
|
QRect getTargetGlobalRect() const;
|
||||||
|
void showTooltip();
|
||||||
|
void hideTooltip();
|
||||||
|
// Hides the tooltip once the pointer is on neither the element nor the tooltip
|
||||||
|
// itself, which is the only thing that dismisses it (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
void hideUnlessPointerHeld();
|
||||||
|
|
||||||
|
QWidget* m_target;
|
||||||
|
Tooltip* m_tooltip;
|
||||||
|
Trigger m_trigger;
|
||||||
|
// Held back so a pointer crossing the element on its way elsewhere does not flash the
|
||||||
|
// tooltip. A click skips it (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
|
QTimer* m_hoverTimer;
|
||||||
|
// Runs only while the tooltip is up, testing where the pointer is. Polled rather than
|
||||||
|
// read off enter/leave events: those arrive in an order that depends on which of the
|
||||||
|
// two widgets the pointer crosses first, and a popup window is not a reliable source
|
||||||
|
// of them.
|
||||||
|
QTimer* m_pointerTimer;
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
#include "ItemChip.h"
|
#include "ItemChip.h"
|
||||||
|
|
||||||
#include <QCursor>
|
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QTimer>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ItemTooltip.h"
|
#include "ItemTooltip.h"
|
||||||
|
#include "TooltipTrigger.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -17,18 +16,12 @@ namespace
|
|||||||
const int kCountSizeRisePt = 2;
|
const int kCountSizeRisePt = 2;
|
||||||
const int kSubLineSizeDropPt = 1;
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId,
|
ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId,
|
||||||
const QPixmap& icon, QWidget* parent)
|
const QPixmap& icon, QWidget* parent)
|
||||||
: 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
|
// 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.
|
// furniture rather than from visuals.toml, which is for world rendering.
|
||||||
@@ -70,10 +63,18 @@ ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId,
|
|||||||
layout->addWidget(m_iconLabel);
|
layout->addWidget(m_iconLabel);
|
||||||
layout->addWidget(text);
|
layout->addWidget(text);
|
||||||
|
|
||||||
m_hoverTimer = new QTimer(this);
|
// Every child is transparent to the mouse, so the chip is what the pointer is on
|
||||||
m_hoverTimer->setSingleShot(true);
|
// wherever it rests on the face -- a child under it would take the chip's own enter,
|
||||||
m_hoverTimer->setInterval(kHoverDelayMs);
|
// leave and press events (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
connect(m_hoverTimer, &QTimer::timeout, this, &ItemChip::showTooltip);
|
m_iconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
m_countLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
m_subLineLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
text->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemChip::setCount(const QString& count)
|
void ItemChip::setCount(const QString& count)
|
||||||
@@ -86,28 +87,3 @@ void ItemChip::setSubLine(const QString& subLine)
|
|||||||
m_subLineLabel->setText(subLine);
|
m_subLineLabel->setText(subLine);
|
||||||
m_subLineLabel->setVisible(!subLine.isEmpty());
|
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());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,16 +8,13 @@
|
|||||||
|
|
||||||
#include "SelectionContext.h"
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
class ItemTooltip;
|
|
||||||
class QEvent;
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
// One buffered item: its icon, its count in a larger type, and a sub-line beneath the
|
// 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
|
// count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities
|
||||||
// rather than as a run of text.
|
// rather than as a run of text.
|
||||||
//
|
//
|
||||||
// Hovering it explains where its item comes from (REQ-UI-ITEM-TOOLTIP).
|
// Hovering or clicking it explains where its item comes from (REQ-UI-ITEM-TOOLTIP).
|
||||||
class ItemChip : public QWidget
|
class ItemChip : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -31,21 +28,8 @@ public:
|
|||||||
// Left empty for an item with nothing to say beneath its count.
|
// Left empty for an item with nothing to say beneath its count.
|
||||||
void setSubLine(const QString& subLine);
|
void setSubLine(const QString& subLine);
|
||||||
|
|
||||||
protected:
|
|
||||||
void enterEvent(QEvent* event) override;
|
|
||||||
void leaveEvent(QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void showTooltip();
|
QLabel* m_iconLabel;
|
||||||
|
QLabel* m_countLabel;
|
||||||
SelectionContext m_context;
|
QLabel* m_subLineLabel;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
#include "ItemTooltip.h"
|
#include "ItemTooltip.h"
|
||||||
|
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLayoutItem>
|
|
||||||
#include <QPalette>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QScreen>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
@@ -19,10 +14,6 @@
|
|||||||
namespace
|
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(
|
std::vector<RecipeLineRow::Amount> toAmounts(
|
||||||
const std::vector<RecipeIngredient>& ingredients)
|
const std::vector<RecipeIngredient>& ingredients)
|
||||||
{
|
{
|
||||||
@@ -35,75 +26,21 @@ std::vector<RecipeLineRow::Amount> toAmounts(
|
|||||||
return amounts;
|
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
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
ItemTooltip::ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
ItemTooltip::ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QFrame(parent, Qt::ToolTip)
|
: Tooltip(parent)
|
||||||
, m_context(context)
|
, m_context(context)
|
||||||
, m_itemId(itemId)
|
, 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)
|
void ItemTooltip::refreshContent()
|
||||||
{
|
{
|
||||||
rebuild();
|
clearContent();
|
||||||
adjustSize();
|
QVBoxLayout* layout = getContentLayout();
|
||||||
|
|
||||||
// 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
|
// 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).
|
// at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION).
|
||||||
@@ -112,7 +49,7 @@ void ItemTooltip::rebuild()
|
|||||||
QFont headingFont = heading->font();
|
QFont headingFont = heading->font();
|
||||||
headingFont.setBold(true);
|
headingFont.setBold(true);
|
||||||
heading->setFont(headingFont);
|
heading->setFont(headingFont);
|
||||||
m_layout->addWidget(heading);
|
layout->addWidget(heading);
|
||||||
heading->show();
|
heading->show();
|
||||||
|
|
||||||
const ItemProduction production =
|
const ItemProduction production =
|
||||||
@@ -123,7 +60,7 @@ void ItemTooltip::rebuild()
|
|||||||
QLabel* caption = new QLabel(production.origin == ItemOrigin::Salvaged
|
QLabel* caption = new QLabel(production.origin == ItemOrigin::Salvaged
|
||||||
? tr("Salvaged from debris")
|
? tr("Salvaged from debris")
|
||||||
: tr("Produced by"), this);
|
: tr("Produced by"), this);
|
||||||
m_layout->addWidget(caption);
|
layout->addWidget(caption);
|
||||||
caption->show();
|
caption->show();
|
||||||
|
|
||||||
if (production.origin == ItemOrigin::Undiscovered)
|
if (production.origin == ItemOrigin::Undiscovered)
|
||||||
@@ -131,7 +68,7 @@ void ItemTooltip::rebuild()
|
|||||||
// Made somehow, but by nothing the player has unlocked -- named without being
|
// Made somehow, but by nothing the player has unlocked -- named without being
|
||||||
// shown a path they cannot take yet (REQ-UI-ITEM-TOOLTIP).
|
// shown a path they cannot take yet (REQ-UI-ITEM-TOOLTIP).
|
||||||
QLabel* undiscovered = new QLabel(tr("Undiscovered"), this);
|
QLabel* undiscovered = new QLabel(tr("Undiscovered"), this);
|
||||||
m_layout->addWidget(undiscovered);
|
layout->addWidget(undiscovered);
|
||||||
undiscovered->show();
|
undiscovered->show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,7 +87,7 @@ void ItemTooltip::rebuild()
|
|||||||
RecipeLineRow* line =
|
RecipeLineRow* line =
|
||||||
new RecipeLineRow(m_context.itemIcons, m_context.buildingIcons, this);
|
new RecipeLineRow(m_context.itemIcons, m_context.buildingIcons, this);
|
||||||
line->setCardChrome(true);
|
line->setCardChrome(true);
|
||||||
m_layout->addWidget(line);
|
layout->addWidget(line);
|
||||||
line->setLine(spec);
|
line->setLine(spec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QFrame>
|
|
||||||
|
|
||||||
#include "SelectionContext.h"
|
#include "SelectionContext.h"
|
||||||
|
#include "Tooltip.h"
|
||||||
|
|
||||||
class QVBoxLayout;
|
// 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).
|
||||||
// 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
|
// Its content is drawn -- item squares, building chips and all -- rather than written,
|
||||||
// drawn -- item squares, building chips and all. It is a Qt::ToolTip window, so it
|
// which is what it adds to the plain text tooltip it derives from. Rebuilt on every show,
|
||||||
// floats above everything and takes no input.
|
// because what produces an item changes as the player unlocks recipes
|
||||||
class ItemTooltip : public QFrame
|
// (REQ-LOCK-UI-RECIPE).
|
||||||
|
class ItemTooltip : public Tooltip
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -22,15 +20,10 @@ public:
|
|||||||
ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
ItemTooltip(const SelectionContext& context, const std::string& itemId,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
// Rebuilds the content and shows the tooltip near the given screen position, kept
|
protected:
|
||||||
// inside the screen. Rebuilt on every show because what produces an item changes as
|
void refreshContent() override;
|
||||||
// the player unlocks recipes (REQ-LOCK-UI-RECIPE).
|
|
||||||
void showAt(const QPoint& globalPos);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rebuild();
|
|
||||||
|
|
||||||
SelectionContext m_context;
|
SelectionContext m_context;
|
||||||
std::string m_itemId;
|
std::string m_itemId;
|
||||||
QVBoxLayout* m_layout;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user