spacr.qt.widgets.availability_panel¶
Explain unavailable options and offer an applicable next step.
A control whose entry is unavailable has three jobs it cannot do while it is honestly dead: say WHY, link the documentation, and – where it is possible in this environment – offer to make itself available. This module is where all three live, once, because the regression backend picker and the Image UMAP’s GPU acceleration ask exactly the same question and a second interactive tooltip is a second set of hover, focus and dismissal bugs.
Why this is not a QToolTip: PySide6 reports:
QToolTip has a linkActivated signal: False
QToolTip is a QWidget: False
QToolTip is a STATIC PAINTER. It renders rich text and nothing inside it
can be clicked, hovered or focused, so an “install link in the tooltip” drawn
with it is a link that looks pressable and is not. What works is a frameless
tooltip-styled QLabel with Qt.TextBrowserInteraction: its
linkActivated carries the href, so api opens the documentation and
install opens the offer.
THE ROW ITSELF STAYS DEAD. QStandardItem.setEnabled(False) leaves
ItemIsSelectable SET – Qt will not ACTIVATE a disabled row, but a
model-level selection can still land on it – so a caller must clear that
flag too. disable_combo_row() does both and is what the callers use.
THE THREE THINGS AN INTERACTIVE TOOLTIP HAS TO GET RIGHT, and how each is answered here:
IT MUST NOT VANISH WHILE THE POINTER MOVES TOWARD IT. The pointer has to cross the gap between the row and the panel, and a plain leave-event dismissal closes it mid-journey, which makes the Install link unreachable. The hide is a timer, the timer is cancelled on entry, and while it is pending the cursor is checked against the CORRIDOR – the union of the anchor’s rectangle and the panel’s – so travelling through the gap re-arms the timer instead of firing it. See
AvailabilityPanel. _maybe_hide().IT MUST BE DISMISSABLE. Escape, a click anywhere outside it, and moving well clear of the corridor all close it.
IT MUST BE REACHABLE BY KEYBOARD, and that cannot be inherited from the row because the row is disabled.
AvailabilityPanel.open_for()is the explicit keyboard route: it shows the panel ACTIVATED with the first link focused, Up/Down move between the unavailable entries, Escape closes it and returns focus to wherever it came from. A keyboard-opened panel is pinned and never closes on a hover timer, because a reader who is not holding the mouse must not have the panel taken away from them.
WHAT PRESSING INSTALL DOES is run_install_offer(), and it has THREE
answers rather than two – see spacr.updater.InstallOffer. The
dry-run report is shown BEFORE anything is installed, and a plan that would
move numpy, torch, pandas or scikit-learn is refused by default and needs a
second confirmation naming what moves.
Classes¶
The hover panel a greyed-out option opens: reason, API, Install. |
Functions¶
|
Disable a combo row and remove it from keyboard selection. |
|
Open the shared panel on |
|
The link word for an offer's |
|
Handle the three outcomes of an install offer in one place. |
Module Contents¶
- class spacr.qt.widgets.availability_panel.AvailabilityPanel[source]¶
Bases:
PySide6.QtWidgets.QFrameThe hover panel a greyed-out option opens: reason, API, Install.
Access through
instance()– it is a process-wide singleton, so one panel serves every control that has an unavailable option and there is only ever one of them on screen.- Signal api_requested:
the API link was pressed; carries the URL.
- Signal install_requested:
the Install link was pressed; carries the
spacr.updater.InstallOfferof the entry on screen.- Signal dismissed:
the panel closed by Escape, by a click away, or by the pointer leaving the corridor.
Build the availability popup.
A
Qt.Toolwindow rather than aQt.ToolTipone: a tooltip window can never take focus, and the keyboard route into this panel needs it to. The two link words are separate labels so that “Install sits to the right of the API link” is a fact about geometry a test can measure, rather than a claim about a string.- api_link() PySide6.QtWidgets.QLabel[source]¶
The API word – exposed so a test can measure where it sits.
- corridor() PySide6.QtCore.QRect | None[source]¶
The rectangle the pointer is allowed to be inside while travelling.
The union of the anchor’s rectangle and the panel’s, which is exactly the region a pointer moving from one to the other passes through. A naive implementation hides on the anchor’s leave event and the pointer never arrives.
- current_offer()[source]¶
The
spacr.updater.InstallOfferon screen, orNone.
- enterEvent(event)[source]¶
The pointer arrived: the panel stays.
- Parameters:
event – the enter event, passed on to the base class after any pending hide is cancelled.
- eventFilter(obj, event)[source]¶
A press anywhere outside the panel dismisses it.
- Parameters:
obj – the watched object; the filter is installed on the application, so this is whatever received the event. Not read.
event – the event; only a mouse press while the panel is visible is acted on, using its global position. Always returns
Falseso the press still reaches its target.
- hideEvent(event)[source]¶
Drop the app filter whenever the panel leaves the screen.
- Parameters:
event – the hide event, passed on to the base class.
- classmethod instance() AvailabilityPanel[source]¶
The process-wide panel, created on first use.
- keyPressEvent(event)[source]¶
Escape closes; Up/Down move between the entries.
- Parameters:
event – the key event; Escape, Up/Left and Down/Right are accepted here (the arrows only with more than one entry), and any other key goes to the base class.
- leaveEvent(event)[source]¶
The pointer left: start the interruptible hide.
- Parameters:
event – the leave event, passed on to the base class after a 100 ms hide is scheduled.
- open_for(anchor: PySide6.QtWidgets.QWidget, entries, index: int = 0, *, anchor_rect: PySide6.QtCore.QRect | None = None) None[source]¶
The KEYBOARD route: show the panel pinned, activated and focused.
The row that would normally carry this help is disabled, so it cannot be tabbed to and nothing can be inherited from it. A caller wires this to a key on the control that IS focusable – the combo itself – and the panel takes it from there: Tab moves between API and Install, Enter presses one, Up/Down move to the next unavailable entry, Escape closes and hands focus back.
- Parameters:
anchor – the widget the panel is docked under; see
show_for().entries – availability mappings (
{title, reason, url, offer}) asshow_for()takes them; an empty list shows nothing.
- set_install_handler(slot) None[source]¶
Make
slotthe ONLY receiver ofinstall_requested.The panel is a process-wide singleton with two callers. A plain
connecton every show accumulates receivers, so one press would eventually run the regression picker’s install AND the Image UMAP’s; a plaindisconnect()warns when nothing is connected yet. Holding the current slot and replacing it does neither. A previous slot bound to a widget that has since been deleted is simply dropped.- Parameters:
slot – callable taking the current entry’s
offer, orNoneto leave the signal with no receiver.
- show_entry(index: int) None[source]¶
Move to another of the entries without moving the panel.
- Parameters:
index – entry position; wraps around modulo the number of entries, so
-1is the last one.
- show_for(anchor: PySide6.QtWidgets.QWidget, entries, index: int = 0, *, anchor_rect: PySide6.QtCore.QRect | None = None, pinned: bool = False, immediate: bool = False) None[source]¶
Show the panel for
entries[index], docked underanchor.- Parameters:
anchor – the widget the panel belongs to. Used for placement and as one half of the corridor the pointer may cross.
entries – mappings as
spacr.regression_backends.availability_entry()returns –{title, reason, url, offer}. More than one makes the panel cyclable with Up/Down when it is pinned.index – which of them to show.
anchor_rect – the anchor’s rectangle in GLOBAL coordinates, when the thing being explained is smaller than the widget – a single row of an open combo popup, for instance.
pinned – opened by keyboard. See
open_for().immediate – explicit click/keyboard request, bypassing hover delay.
- spacr.qt.widgets.availability_panel.disable_combo_row(combo, index: int, *, tooltip: str = '') None[source]¶
Disable a combo row and remove it from keyboard selection.
QStandardItem.setEnabled(False)leavesQt.ItemIsSelectableset in the item’s flags. Qt refuses to activate a disabled row from the popup, so the mouse route is closed, but a model-level selection (setCurrentIndex, a settings CSV round-trip, a view’s own selection model) can still land on it. An unavailable entry must not become the selected value, so this function clears both flags.A disabled item retains its
Qt.ToolTipRole, so the explanatory tooltip remains available on hover.- Parameters:
combo –
QComboBoxbacked by aQStandardItemModel.index – Row to disable.
tooltip – Explanatory tooltip; an empty string leaves it unchanged.
- spacr.qt.widgets.availability_panel.explain(anchor, entries, index: int = 0, *, pinned: bool = False, parent=None, anchor_rect=None, on_installed=None)[source]¶
Open the shared panel on
entries[index]with Install already wired.The two-line form for a caller that has one control and one entry – the Image UMAP’s GPU acceleration is exactly that:
from spacr.gpu_reduce import availability_entry explain(self._gpu_button, [availability_entry()], parent=self)
The panel is a process-wide singleton, so the Install connection is replaced on every call rather than accumulated; two callers connected at once would both answer one press.
- Parameters:
anchor – the widget the panel docks under.
entries – mappings from
availability_entry– seeAvailabilityPanel.show_for().pinned – open it by the keyboard route (activated and focused).
parent – what the install dialogs are parented to; defaults to
anchor.on_installed – called with the offer after a successful install, for a caller that has to re-probe.
- Returns:
the panel, or
Nonewhenentriesis empty.
- spacr.qt.widgets.availability_panel.install_word_for(action) str[source]¶
The link word for an offer’s
action;''when there is none.- Parameters:
action – the offer’s action,
"install","elsewhere","impossible"or"ready"(which gives''); any other value gives"What it needs".
- spacr.qt.widgets.availability_panel.run_install_offer(parent, offer, *, confirm=None, inform=None, dry_run=None, install=None) str[source]¶
Handle the three outcomes of an install offer in one place.
INSTALLABLE HERE – the dry-run report is shown FIRST, in full, and the install runs only on confirmation. A plan that would move numpy, torch, pandas or scikit-learn is refused by default and needs a second confirmation naming what moves.
NOT HERE, BUT POSSIBLE ELSEWHERE – the environment that would take it is described and NOTHING IS RUN.
NOT POSSIBLE – said plainly, with the recipe, and nothing is run.
Every side effect is an injected callable so the whole flow can be driven without a screen:
tests/qt/conftest.pymakes a static modal raise on purpose, and a flow that could only be tested through one could not be tested at all.- Parameters:
parent – the widget dialogs are parented to.
offer – a
spacr.updater.InstallOffer.confirm –
(title, text) -> bool. Defaults to aQMessageBox.questionwhose default button is No.inform –
(title, text) -> None. Defaults toQMessageBox.information.dry_run –
(requirement) -> DryRun. Defaults tospacr.updater.dry_run_install()behind a progress dialog – the resolver talks to the network and has been measured taking minutes, and a frozen window is indistinguishable from a crash.install –
(command) -> (code, output). Defaults tospacr.updater.run_install_command().
- Returns:
one of
'ready','explained','declined','refused','installed','failed'.
Nested helpers¶
- _default_confirm._confirm(title: str, text: str) bool¶
Ask a yes/no question in a modal box.
spacr/qt/widgets/availability_panel.py:781
- _default_dry_run._run(requirement)¶
Dry-run an install and hand the outcome back.
spacr/qt/widgets/availability_panel.py:819
- _default_dry_run._run._work()¶
Do the dry run. Called off the GUI thread.
spacr/qt/widgets/availability_panel.py:823
- _default_inform._inform(title: str, text: str) None¶
Say something in a modal box.
spacr/qt/widgets/availability_panel.py:796
- explain._install(offer)¶
Run one install offer and report what happened.
spacr/qt/widgets/availability_panel.py:880