spacr.qt.widgets.hover_tooltip

HoverTooltip — a QFrame-based popup that stays visible when the mouse enters it. Unlike QToolTip, users can move their cursor into the popup to click links inside.

Usage:

tip = HoverTooltip.instance()
tip.show_for(some_widget, "some html")   # on hover-enter
tip.start_hide()                          # on hover-leave

The popup cancels its own hide timer if the mouse enters it, and only actually hides when neither the anchor nor the popup itself is under the cursor.

A hover shows text only. No GIF is decoded, no frames are cached and no timer runs until the reader asks for the animation: 141 settings have one, and each is a ~73 ms decoded movie. A hover that only wanted the sentence should not pay for one. Measured, a sweep of all 141: 0 decodes, 2.6 ms a hover.

Asked for, the animation appears to the RIGHT of the text. Both columns start at the same top edge, so the first line of prose and the first frame are read together rather than one being hunted for beside the other:

+-----------------------------+-----------------------------+
| Cell diameter (int)         |                             |
| Expected cell diameter in   |         (animation)         |
| pixels...                   |                             |
| API  Animation              |                             |
+-----------------------------+-----------------------------+

The text column is exactly as tall as the animation square and no taller: its width is widened, one step at a time, until the prose fits inside the square’s height. With no animation beside it the popup shrinks to the text — nothing is padded out to a shape it does not need.

The last line is two words, not a sentence: API in the theme accent opens the same documentation page the old Open spaCR API documentation link did, and Animation in teal reveals the square — or folds it away again.

That reveal is PER SETTING. Pressing Animation on cell_diameter shows cell_diameter’s animation and nothing else; move to the next setting and it is hidden again until its own Animation is pressed. A session-wide reveal was the obvious alternative and is exactly wrong for the machines this was asked for: one click would put every later hover back on the ~73 ms decode path for the rest of the run, which is the cost the change exists to avoid. Measured, the same sweep of 141 settings taken straight after a press: still 0 decodes.

Re-hovering the SAME setting keeps its reveal, so moving the pointer between a label and the popup below it does not fight the reader. The state is one key and one bool — see HoverTooltip.animations_shown().

Nothing is decoded before a press. The Animation word is offered from a registry lookup, which reads no pixels; the GIF is read, measured, cropped, zoomed and rounded only when the word is pressed. Two caches sit under that, and neither is ever filled speculatively:

  • spacr.qt.widgets.animation_zoom.zoomed_animation() already keeps the eight most recent zooms (~2.6 MB each), which is what turns a repeat press from ~73 ms into ~2.9 ms;

  • this widget keeps the finished pixmaps of ONE animation — the one last revealed, ~3.5 MB — so folding a setting away and back is free. They are dropped the moment the pointer moves to a different setting.

The Setting animations preference is the escape hatch for a reader who wants them always: on, every tooltip starts revealed and the word folds THIS one away; off — the default — every tooltip starts hidden and the word reveals THIS one. Because a press only ever names one setting, it can never leave the preference unable to take effect.

Which animation is decided by the anchor’s settingKey property, so no caller has to pass one; the callers that put help on a label already set it. Anything without that property — a section header, a home tile — gets a text-only popup with no Animation word to click.

The popup is ONE surface. Its two layout containers paint nothing, so the rounded grey frame is the only fill and the page-opacity preference moves it as a single layer — see HoverTooltip._apply_theme() for the black slab that taught us.

Only one tooltip ever appears. The screens that anchor this popup also leave a native Qt tooltip on the same label (refresh_api_tooltips re-applies it on every Enter, for the accessibility tree), and Qt’s own tooltip timer would pop that up a second later, on top of this one — two tooltips, one after the other. Claiming an anchor therefore installs _NativeTooltipSuppressor on it, which swallows QEvent.ToolTip while leaving toolTip() intact for screen readers.

Classes

HoverTooltip

Sticky QFrame popup that survives cursor entry so users can click links.

Functions

split_api_link(→ Tuple[str, str])

Split a trailing documentation link off a tooltip body.

Module Contents

class spacr.qt.widgets.hover_tooltip.HoverTooltip[source]

Bases: PySide6.QtWidgets.QFrame

Sticky QFrame popup that survives cursor entry so users can click links.

Access via instance() — the popup is a process-wide singleton.

Build the process-wide hover popup.

A tool-tip window with our own painting: shown without activating, so it never takes focus from what the pointer is over.

animation()[source]

The animation currently shown beside the text, or None.

The teal Animation word that toggles the square.

animation_view() → _AnimationView[source]

The square animation panel — exposed for layout tests.

animations_shown() → bool[source]

Whether the setting currently hovered shows its animation.

Off unless this setting was asked for. A press on Animation names one setting; every other setting falls back to the Setting animations preference, which means “show animations without asking” and defaults, like this, to off.

Scoped to a setting rather than to the session on purpose: a reveal that outlived the setting would put every later hover back on the ~73 ms decode path after a single press, which is the cost the reader was trying to avoid. Nothing here needs to guard the preference either — a press cannot reach past the setting it named.

Read on every hover, never cached: the popup is a process-wide singleton that outlives the Preferences dialog.

The blue API word.

api_url() → str[source]

Documentation URL taken out of the body, or "".

cancel_hide() → None[source]

Cancel any pending hide timer (called on cursor re-entry).

enterEvent(event)[source]

Cancel the hide timer when the cursor enters the popup.

Parameters:

event – the enter event; passed to the base class and otherwise not read.

hideEvent(event)[source]

Stop decoding frames the moment the popup leaves the screen.

The popup is a singleton, so without this its timer would keep swapping pixmaps into an invisible label for the rest of the session after the last hover.

Parameters:

event – the hide event; passed to the base class after the animation stops.

classmethod instance() → HoverTooltip[source]

Return the process-wide singleton, creating it on first access.

leaveEvent(event)[source]

Restart the hide timer when the cursor leaves the POPUP itself.

Shorter than the anchor’s grace period on purpose: leaving the popup is a deliberate act, where leaving the label may just be the journey towards it.

Parameters:

event – the leave event; passed to the base class and otherwise not read.

offered_animation()[source]

The animation this anchor has, shown or collapsed by the toggle.

open_api_documentation() → None[source]

Open the documentation page the body’s trailing link pointed at.

showEvent(event)[source]

Resume the loaded animation when the popup comes back.

Parameters:

event – the show event; passed to the base class and otherwise not read.

show_for(anchor: PySide6.QtWidgets.QWidget, html: str, animation=_DERIVE, *, immediate: bool = False) → None[source]

Show the tooltip beneath anchor with body html.

Parameters:
  • anchor – widget the popup docks to (clamped to its screen).

  • html – rich-text body; empty strings are ignored. A trailing documentation link is moved out of the prose and into the API word at the foot of the popup.

  • animation – a spacr.setting_animations.SettingAnimation to play beside the text, or None for text only. Left out, it is derived from the anchor’s settingKey property — every caller that attaches setting help already sets that, so none of them had to change.

  • immediate – show now for an explicit click or an already delayed hover callback; ordinary hover callers leave this false.

start_hide(delay_ms: int = 0) → None[source]

Schedule a hide after delay_ms unless the cursor re-enters.

0 means HIDE_DELAY_MS – the default is named rather than written into the signature so every caller moves together.

text_column() → PySide6.QtWidgets.QWidget[source]

The prose and the two link words, as one block.

text_label() → PySide6.QtWidgets.QLabel[source]

The explanation panel — exposed for layout tests.

toggle_animation() → None[source]

Reveal this setting’s animation, or fold it away again.

Deliberately not written to spacr.qt.preferences, and deliberately naming one setting. This is the reader asking to see this animation; the preference is the reader asking to stop being asked about any of them. Because the press names a setting, it cannot turn animations on for the next one, and it cannot leave the preference unable to take effect.

toggled_setting() → str | None[source]

The one setting a press has spoken for, or None — for tests.

Split a trailing documentation link off a tooltip body.

settings_model.format_tooltip ends every setting’s help with <a href="...">Open spaCR API documentation</a>. The popup renders that destination as its own API word instead, so the anchor is taken out of the prose here rather than in the formatter — the same string is still used verbatim by the hint strip, the accessibility tree and every other consumer of format_tooltip.

Only a link that really is the last thing in the body is taken; a link inside a sentence stays where the author put it.

Parameters:

html – the tooltip body as rich text; the last <a href> anchor is removed only when nothing but whitespace follows it, along with the line breaks before it.

Returns:

(body_without_the_link, url); url is "" when there was no trailing link.