carry a tooltip along with the element it explains
Scrolling the world under an open tooltip left it stranded beside where its button used to be. It now holds the place it was given in relation to its element instead of the place it was given on the screen, so the element takes it along -- out from under the pointer, and off the screen if that is where the element goes, which is what then hides it. Followed on a timer rather than on move events: a move event reaches only the widget whose own position changed, never a label carried along by the button around it, which is exactly the case here. The timer already ran while a tooltip was up; it now ticks once a frame so the tooltip keeps up with the ground it stands on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -442,7 +442,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
|||||||
Several UI elements carry a tooltip: the header bar's artifact count (REQ-UI-ARTIFACTS-TOOLTIP), every display that names an item — item chips, inline item values, and recipe summary icons, the header bar's building blocks stock among them (REQ-UI-ITEM-TOOLTIP, REQ-UI-ITEM-VALUE-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 first two requirements here define how every one of them is shown and hidden, and the third says which displays carry an item's tooltip without being an item chip. An element not named there describes itself or says nothing: 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) — carrying no tooltip of their own, though the items they name still explain themselves (REQ-UI-ITEM-VALUE-TOOLTIP).
|
Several UI elements carry a tooltip: the header bar's artifact count (REQ-UI-ARTIFACTS-TOOLTIP), every display that names an item — item chips, inline item values, and recipe summary icons, the header bar's building blocks stock among them (REQ-UI-ITEM-TOOLTIP, REQ-UI-ITEM-VALUE-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 first two requirements here define how every one of them is shown and hidden, and the third says which displays carry an item's tooltip without being an item chip. An element not named there describes itself or says nothing: 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) — carrying no tooltip of their own, though the items they name still explain themselves (REQ-UI-ITEM-VALUE-TOOLTIP).
|
||||||
|
|
||||||
- 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. Nor does it cover an item named on such a control (REQ-UI-ITEM-VALUE-TOOLTIP): the click there belongs to the control, whatever the pointer is resting on. 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. Nor does it cover an item named on such a control (REQ-UI-ITEM-VALUE-TOOLTIP): the click there belongs to the control, whatever the pointer is resting on. 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 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-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. **A tooltip whose element moves goes with it**, holding the place it was given in relation to that element rather than the place it was given on the screen: the expansion button rides the scrolling world (REQ-UI-EXPAND-BUTTON), and a tooltip left standing where the button used to be reads as belonging to nothing. Moving with it is also what ends it, without any rule of its own — the element travels out from under the pointer and takes the tooltip along, and once the pointer is on neither, 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.
|
- 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), 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: <n> 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: <n> Blocks`, `Scrap remaining`), the value still states an amount of an item and still explains it.
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ namespace
|
|||||||
// One delay for every tooltip in the UI, now that none of them is Qt's (REQ-UI-TOOLTIP-TRIGGER).
|
// One delay for every tooltip in the UI, now that none of them is Qt's (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
const int kHoverDelayMs = 500;
|
const int kHoverDelayMs = 500;
|
||||||
|
|
||||||
// How often the pointer is tested against the element and the tooltip while the tooltip
|
// How often a visible tooltip is carried after its element and tested against the
|
||||||
// is up, in milliseconds. Short enough to read as immediate, and it runs only then.
|
// pointer, in milliseconds. A frame's worth: the element it follows can move with the
|
||||||
const int kPointerPollMs = 100;
|
// scrolling world (REQ-UI-EXPAND-BUTTON), and anything slower would visibly drag behind
|
||||||
|
// it. It runs only while a tooltip is up.
|
||||||
|
const int kTrackingIntervalMs = 16;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -103,10 +105,14 @@ TooltipTrigger::TooltipTrigger(QWidget& target, Tooltip& tooltip, Trigger trigge
|
|||||||
m_hoverTimer->setInterval(kHoverDelayMs);
|
m_hoverTimer->setInterval(kHoverDelayMs);
|
||||||
connect(m_hoverTimer, &QTimer::timeout, this, &TooltipTrigger::showTooltip);
|
connect(m_hoverTimer, &QTimer::timeout, this, &TooltipTrigger::showTooltip);
|
||||||
|
|
||||||
m_pointerTimer = new QTimer(this);
|
m_trackingTimer = new QTimer(this);
|
||||||
m_pointerTimer->setInterval(kPointerPollMs);
|
m_trackingTimer->setInterval(kTrackingIntervalMs);
|
||||||
connect(m_pointerTimer, &QTimer::timeout,
|
connect(m_trackingTimer, &QTimer::timeout, this, [this]() {
|
||||||
this, &TooltipTrigger::hideUnlessPointerHeld);
|
// Carried along first, so that a tooltip that has just been pulled out from under
|
||||||
|
// the pointer is found to be out from under it on this same tick.
|
||||||
|
followTarget();
|
||||||
|
hideUnlessPointerHeld();
|
||||||
|
});
|
||||||
|
|
||||||
m_target->installEventFilter(this);
|
m_target->installEventFilter(this);
|
||||||
}
|
}
|
||||||
@@ -127,26 +133,46 @@ void TooltipTrigger::showTooltip()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_tooltip->showAt(QCursor::pos());
|
m_tooltip->showAt(QCursor::pos());
|
||||||
m_pointerTimer->start();
|
m_targetGlobalRect = getTargetGlobalRect();
|
||||||
|
m_trackingTimer->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TooltipTrigger::hideTooltip()
|
void TooltipTrigger::hideTooltip()
|
||||||
{
|
{
|
||||||
m_pointerTimer->stop();
|
m_trackingTimer->stop();
|
||||||
m_tooltip->hide();
|
m_tooltip->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TooltipTrigger::followTarget()
|
||||||
|
{
|
||||||
|
const QRect targetRect = getTargetGlobalRect();
|
||||||
|
const QPoint travelled = targetRect.topLeft() - m_targetGlobalRect.topLeft();
|
||||||
|
m_targetGlobalRect = targetRect;
|
||||||
|
if (travelled.isNull())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tooltip holds the place it was given in relation to its element rather than the
|
||||||
|
// place it was given on the screen, so an element that travels -- the expansion button
|
||||||
|
// riding the scrolling world (REQ-UI-EXPAND-BUTTON) -- takes its tooltip with it
|
||||||
|
// instead of leaving it stranded beside where it used to be. Not clamped back onto
|
||||||
|
// the screen this time: following the element off the edge is the point, and the
|
||||||
|
// pointer left behind is what then hides it (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
m_tooltip->move(m_tooltip->pos() + travelled);
|
||||||
|
}
|
||||||
|
|
||||||
void TooltipTrigger::hideUnlessPointerHeld()
|
void TooltipTrigger::hideUnlessPointerHeld()
|
||||||
{
|
{
|
||||||
if (!m_tooltip->isVisible())
|
if (!m_tooltip->isVisible())
|
||||||
{
|
{
|
||||||
m_pointerTimer->stop();
|
m_trackingTimer->stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gone with the element it explains. An element that merely moves -- a header label
|
// Gone with the element it explains. An element that merely moves is not a dismissal:
|
||||||
// resized by the number it carries -- is not a dismissal: the pointer is still on the
|
// the tooltip travels with it (followTarget), and nothing but the pointer dismisses
|
||||||
// tooltip, and nothing but the pointer dismisses one (REQ-UI-TOOLTIP-DISMISS).
|
// one (REQ-UI-TOOLTIP-DISMISS).
|
||||||
if (!m_target->isVisible())
|
if (!m_target->isVisible())
|
||||||
{
|
{
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ private:
|
|||||||
QRect getTargetGlobalRect() const;
|
QRect getTargetGlobalRect() const;
|
||||||
void showTooltip();
|
void showTooltip();
|
||||||
void hideTooltip();
|
void hideTooltip();
|
||||||
|
// Keeps the tooltip where it was put in relation to its element, so an element that
|
||||||
|
// moves takes its tooltip along (REQ-UI-TOOLTIP-DISMISS).
|
||||||
|
void followTarget();
|
||||||
// Hides the tooltip once the pointer is on neither the element nor the tooltip
|
// 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).
|
// itself, which is the only thing that dismisses it (REQ-UI-TOOLTIP-DISMISS).
|
||||||
void hideUnlessPointerHeld();
|
void hideUnlessPointerHeld();
|
||||||
@@ -53,9 +56,14 @@ private:
|
|||||||
// Held back so a pointer crossing the element on its way elsewhere does not flash the
|
// 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).
|
// tooltip. A click skips it (REQ-UI-TOOLTIP-TRIGGER).
|
||||||
QTimer* m_hoverTimer;
|
QTimer* m_hoverTimer;
|
||||||
// Runs only while the tooltip is up, testing where the pointer is. Polled rather than
|
// Runs only while the tooltip is up: it carries the tooltip along with an element
|
||||||
// read off enter/leave events: those arrive in an order that depends on which of the
|
// that moves and tests where the pointer is. Polled rather than read off events:
|
||||||
// two widgets the pointer crosses first, and a popup window is not a reliable source
|
// enter and leave arrive in an order that depends on which of the two widgets the
|
||||||
// of them.
|
// pointer crosses first and a popup window is not a reliable source of them, and a
|
||||||
QTimer* m_pointerTimer;
|
// move event reaches only the widget whose own position changed -- never a label
|
||||||
|
// carried along by the button around it, which is the case that matters here.
|
||||||
|
QTimer* m_trackingTimer;
|
||||||
|
// Where the element was when the tooltip was last placed, so the next tick can tell
|
||||||
|
// how far it has travelled since. Only meaningful while the tooltip is up.
|
||||||
|
QRect m_targetGlobalRect;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user