spacr.qt.app

QApplication bootstrap + MainWindow.

launch(argv) is the public entry point called by spacr-qt and python -m spacr.qt.

Classes

MainWindow

Top-level window: sidebar + stacked screens + status bar.

Sidebar

The application's navigation dock.

Functions

app_is_visible(→ bool)

Whether key should appear in module navigation.

app_stage(→ str)

How finished key is — one of STAGES.

dock_rows(→ List[Tuple[str, str, str, str]])

The dock's TOP-LEVEL rows: Home's tiles, then Help, in that order.

folded_children(→ Dict[str, Tuple[str, ...]])

host key -> the module keys folded onto its masthead.

home_bands(→ List[Tuple[str, List[Tuple[str, str, str, ...)

(band, rows) for the Home tab, in SECTIONS order.

home_categories(→ List[Tuple[str, List[str]]])

(section, [app key]) for every tab after Home, in tab order.

home_stages(→ dict)

app key → stage, for every app in APPS.

install_the_dialog_filters(→ tuple[str, ...])

Install the registered dialog event filters on a Qt application.

install_the_spaceout_fractal(→ bool)

Put the spaceout fractal behind screen, if this is spaceout.

launch(→ int)

Bootstrap QApplication and show the main window.

make_home_page([parent])

Build the Home page exactly as the running app builds it.

register_app(→ Tuple[str, str, str, str])

Add one app to the registry. The seam a new module registers through.

registered_entry(key)

Import and return the pipeline callable app key registered.

registered_factory(key)

The factory registered for key, or None.

registered_metadata(→ dict)

{app key: value} for one APP_META field, empty ones dropped.

section_members(→ List[Tuple[str, str, str, str]])

The APPS rows a category's tab shows, in registry order.

tile_sort_key(→ Tuple[int, int])

Sort key placing row where SECTION_TILE_ORDER says.

tiled_apps(→ List[Tuple[str, str, str, str]])

The APPS rows that get a TILE, in registry order.

unregister_app(→ bool)

Remove app key from the registry. True if there was one.

visible_apps(→ List[Tuple[str, str, str, str]])

Registry rows allowed by the Alpha/Beta visibility preferences.

Module Contents

class spacr.qt.app.MainWindow(initial_app: str | None = None)[source]

Bases: PySide6.QtWidgets.QMainWindow

Top-level window: sidebar + stacked screens + status bar.

Parameters:

initial_app – optional app key to navigate to on show; when omitted the window opens on the Home startup page.

Build the main window: the dock slot, the screen stack and the status bar.

The window is frameless and the menu bar is what you drag it by. Its first backing store is filled with the splash colour so the compositor cannot expose stale desktop pixels before anything has drawn – deliberately without WA_OpaquePaintEvent, which is a promise to paint every pixel that this window cannot keep once the application stylesheet clears autoFillBackground again. Claiming it left whatever was already on screen underneath, and transparent children drew on top: overlapping text in the corner, and flicker on every text surface over the animated backdrop.

The dock slot exists in every dock mode, because a QMainWindow’s central widget cannot be swapped without re-parenting the stack, and re-parenting a stack holding live screens is how locking the dock would cost you the screen you were looking at.

Parameters:

initial_app – the module to open on startup; None opens Home.

__init__(initial_app: str | None = None)[source]

Build the main window: the dock slot, the screen stack and the status bar.

The window is frameless and the menu bar is what you drag it by. Its first backing store is filled with the splash colour so the compositor cannot expose stale desktop pixels before anything has drawn – deliberately without WA_OpaquePaintEvent, which is a promise to paint every pixel that this window cannot keep once the application stylesheet clears autoFillBackground again. Claiming it left whatever was already on screen underneath, and transparent children drew on top: overlapping text in the corner, and flicker on every text surface over the animated backdrop.

The dock slot exists in every dock mode, because a QMainWindow’s central widget cannot be swapped without re-parenting the stack, and re-parenting a stack holding live screens is how locking the dock would cost you the screen you were looking at.

Parameters:

initial_app – the module to open on startup; None opens Home.

apply_dock_mode(mode: str | None = None) → None[source]

Put the app list where the preference says it goes.

Two modes, one Sidebar object:

locked the sidebar sits in the window’s dock slot, where it

is an ordinary column in the layout: it never animates, and because it is a LAYOUT MEMBER rather than an overlay the page beside it is narrower by the dock’s width instead of running underneath it.

hidden the slot stays empty. The “All apps” action is

disabled with a tooltip that says where to turn it back on — a control that silently does nothing is worse than one that is greyed out.

There is no reveal-on-hover mode: a dock that slides in over the page covers the Home tiles, and needs a second container behind its panel to stay legible. The drawer object still exists and is kept closed and disabled, because it also carries the keyboard path that the “All apps” action uses.

Idempotent, and safe to call before the menu exists.

changeEvent(event)[source]

Re-lay the menu bar when the window state changes.

A menu opens at the rectangle the menu bar holds for its action. Going fullscreen resizes the bar and its corner widget in one step, and a menu opened before the layout has caught up is placed against the previous action rectangle – which is how pressing spaCR drops a menu under Help.

Parameters:

event – the change event; only a window-state change re-lays the menu bar.

closeEvent(event)[source]

Cooperatively drain analysis and UI workers before destruction.

Parameters:

event – the close event; ignored, leaving the window open, when a worker or an application screen does not stop, otherwise passed to the base class, and the application quits if it is accepted.

dock_mode() → str[source]

The user’s dock preference — auto / locked / hidden.

Read through here rather than inlined so a headless build, or one with an unwritable settings file, still gets the default rather than an exception during __init__.

eventFilter(watched, event)[source]

Handle frameless-window menu-bar drag and double-click gestures.

Teardown can invalidate the Qt menu bar while events are still queued. This filter returns False instead of allowing that condition to escape through PySide’s callback boundary.

Parameters:
  • watched – Qt object receiving the event.

  • event – Qt event delivered to the main window’s filter.

Returns:

True when a menu-bar mouse gesture is consumed; otherwise False.

keyPressEvent(event) → None[source]

Up and Down change the spaceout zoom rate; Ctrl+R starts over.

Parameters:

event – the key press; its key and Ctrl modifier are read, and keys the backdrop does not take are passed to the base class.

open_module(app_key: str) → str[source]

Navigate to the screen that carries app_key, folded or not.

A module folded into a host keeps its key everywhere a key is saved: a run journal, the force-restart record, spacr-qt <app> in somebody’s shell history. That key no longer names a screen, and navigating to it directly would build one – an orphan page with no sidebar row, no tile and no way back to it.

So the key is resolved to the host that took it over, and the fold it names is switched on: asking for Timelapse is asking for what the tracking switch reveals, not for mask generation with the switch off.

Parameters:

app_key – the key that was asked for.

Returns:

the app key actually opened.

pin_all_menu_roles() → None[source]

Give every menu-bar action an explicit macOS role. Idempotent.

Called at the end of _build_menu_bar AND again from spacr.qt.shortcuts.install, because recipes, walkthrough and feature_dictionary all add to Help afterwards. Each of those also pins its own – defence in depth – but a central re-sweep is what makes a module added later safe without its author knowing this problem exists.

rebuild_app_screen(key: str, values=None) → None[source]

Build key’s screen again, carrying values across.

Parameters:
  • key – the app whose screen is to be rebuilt.

  • values – settings to apply to the new screen.

The whole screen is rebuilt because which settings a form holds depends on a few of its own values – the organelle count, and whether an object’s channel names a plane – so a committed change to one of those means a different form, not a changed one. Rebuilding is the same path every module open already takes.

The old screen is dropped from the cache and destroyed, so the new one is built from scratch rather than reusing widgets that belong to a shape that no longer applies.

refresh_after_preferences(changed) → str[source]

Redraw what a Preferences Apply changed, and nothing else.

THE APPLY THAT CHANGED ONE SLIDER REBUILT HOME. Page opacity is in the window’s signature, so moving it ran refresh_theme() – Home rebuilt (2.1-2.6 s on a loaded box), the tree retranslated (0.3 s), every alpha gate walked – while the only things on Home that had taken the opacity were three rules, which HomePage.refresh_pane_surfaces() sets again in place.

Parameters:

changed – names from the window’s signature whose values changed (spacr.qt.preferences._window_changes()).

Returns:

"rebuilt", "in place" or "nothing", so a test can say which.

refresh_language() → None[source]

Apply the persisted language to existing static UI text.

refresh_theme() → None[source]

Rebuild everything preferences cannot update through QSS alone.

Three things do not follow a setStyleSheet call: the Home tiles set sizes/margins from the font scale in Python, every QIcon baked its pixmap at the theme in force when it was built, and the dock’s mode and the page’s opacity are layout decisions rather than colours. Called after the Preferences dialog closes, and found by duck-typing from spacr.qt.preferences.apply_preferences_to_app() so a preference change from anywhere reaches the widgets.

resizeEvent(event)[source]

Keep the loading screen covering the whole window, and erase the menu bar’s old band.

This runs on every resize, not only on a state change. Dragging an edge moves the corner buttons without changing the window state, so changeEvent does not fire, and without this the marks would be redrawn at their new x over the old ones.

Parameters:

event – the resize event, passed to the base class; the new geometry is read from the window itself.

resume_after_restart() → str[source]

Reopen the module a Force restart saved. Returns its key, or “”.

The state is taken, not read: restart_state.take deletes it as it hands it over, so a crash during startup cannot make spaCR reopen the same failing module on every later launch.

The settings are applied and the run is not started: only the configuration is restored, not the runs.

show_preferences_on(tab: str = '', label: str = '') → bool[source]

Open Preferences, on a named tab, with a named row marked.

The route the Help search field takes for a preference result: the dialog carries nine tabs and a result that opened it on whichever one it happened to start with has answered half the question.

The tab is found by object name rather than by index or caption. An index moves whenever a tab is added, and a caption is translated – a Korean interface would have matched nothing. Every page sets its own PreferencesTab* name, and tools/build_help_search_index.py reads those same names out of this dialog’s source, so the two ends of the hand-off are the same string by construction.

The dialog is usually built already (spacr.qt.preferences._warm_the_preferences()), and the window redraws after the close only when something it draws from changed: the build, the first show and the unconditional redraw each held the GUI thread for longer than a frame.

Parameters:
  • tab – the page’s object name, e.g. "PreferencesTabTheme"; "" opens the dialog as it opens from the menu.

  • label – the row caption to mark; "" marks nothing.

Returns:

True when the named tab was found and shown.

snap_window(edge, screen=None) → bool[source]

Snap the window to one side of a screen’s work area.

"up" maximises (the platform’s own maximised state, inside the work area that excludes the taskbar, dock or panels); "left" and "right" take the left or right half and "down" the lower half of QScreen.availableGeometry(). Full screen is a separate state (toggle_fullscreen()) and is left first if it is on. Work areas are in logical pixels, so display scaling is Qt’s.

The size before the first snap is kept in _snap_restore and comes back when the window is dragged off its tile or restored.

On Wayland a client cannot place its own top-level window: the half tiles are applied as a size and the compositor decides the position, so they report False. Maximising is a state request every compositor honours. Dragging the title bar on Wayland goes through startSystemMove and the compositor’s own snapping instead.

Parameters:
  • edge – "left", "right", "up" or "down".

  • screen – the screen to snap on; defaults to the window’s own.

Returns:

whether the requested geometry was applied.

stylesheet_roots()[source]

The widgets that carry the application sheet, instead of me.

WHY NOT THE WINDOW. Setting a stylesheet on a window repolishes every descendant, and a session that has opened four modules owns 8,002 widgets of which 7,595 are inside module screens – ONE of which is on screen. Measured on this box, four modules open, alternating dark and light:

the whole window 945 ms first, ~1,479 ms steady chrome + the visible page 300 ms first, ~310 ms steady

WHAT IS IN THE LIST, and the shape is not the obvious one. The chrome is 407 widgets and only TWO of them are reachable from the central widget – the dock, the sidebar and the status strip hang off the window itself. An implementation that walked the central widget would leave 405 unstyled and look almost right, which is why tests/qt/test_every_widget_wears_the_current_theme.py does exactly that on purpose and asserts it is caught.

So: every direct child of the window except the central widget, every direct child of the central widget except the stack, and the stack’s CURRENT page. The pages that are not showing are marked instead, and sheeted on their own showEvent before they are painted.

A child that is a window of its own – a dialog parented here, such as Preferences – is not chrome: the sheet reaches it as a window, and a hidden one that asked to wait for its show (spacr.qt.theme.SHEETED_WHEN_SHOWN) waits.

toggle_app_drawer() → None[source]

Put keyboard focus on the app dock. The menu and Ctrl+Shift+A path.

The dock does not slide, so there is nothing to open: it is a permanent column when the preference is locked. The action moves keyboard focus to its first row, so a keyboard user does not have to tab through the page to reach it.

A no-op when the dock is hidden, so the shortcut does not override the preference. The action is also disabled in that mode; this check is a second guard.

toggle_fullscreen(*_args) → bool[source]

Enter or leave true fullscreen. Returns whether it is now full.

Leaving returns to the state before: maximised, a tile, or the ordinary window. Independent of the top snap, which maximises.

wheelEvent(event) → None[source]

The wheel does the same, a notch at a time.

Parameters:

event – the wheel event; its vertical angle delta is converted to notches of 120.

window_backdrop()[source]

The one backdrop behind the dock AND the page, or None.

Read by the screens so they do not build a second one on top of it – see _install_screen_backdrop().

class spacr.qt.app.Sidebar(parent=None)[source]

Bases: spacr.qt.widgets.dock.Dock

The application’s navigation dock.

The behaviour lives in spacr.qt.widgets.dock.Dock. This class only binds it to the registry, and it keeps the name Sidebar because the theme’s #Sidebar rules, the tutorial highlighter, the home-variant generators and the tests all reach the dock by it.

The module docstring of spacr.qt.widgets.dock lists the parts of the earlier _DockRow/Sidebar implementation and what replaced each one.

Parameters:

parent – parent widget; ownership only.

Build the dock with Home above the registered module rows.

Parameters:

parent – parent widget, or None.

spacr.qt.app.app_is_visible(key: str) → bool[source]

Whether key should appear in module navigation.

Preferences are imported lazily so this registry remains safe for packaging and headless callers. If preferences cannot be read, preserve the historical all-modules-visible behaviour.

This is not a tile check. It answers whether the user may reach this module, which depends only on the maturity preference. A folded module is still reachable – from its host’s button, from Help, and from the command palette – so it is visible here although it has no tile. Use tiled_apps() to ask whether a module has a tile.

This function did briefly answer both, and the command palette is what caught it: filtering TILELESS_APPS out here took nine modules out of Ctrl+K, so the folds removed a door instead of moving one. Two questions, two functions.

Parameters:

key – the app key whose stage is checked against the maturity preference.

spacr.qt.app.app_stage(key: str) → str[source]

How finished key is — one of STAGES.

Unknown keys read as stable rather than raising: a stage is an annotation on an app, and an app with no annotation is one nobody has flagged.

Parameters:

key – the app key looked up in APP_STAGE; unknown keys give STAGE_STABLE.

spacr.qt.app.dock_rows() → List[Tuple[str, str, str, str]][source]

The dock’s TOP-LEVEL rows: Home’s tiles, then Help, in that order.

A row is a dock host if, and only if, Home draws a tile for it. Everything else in the registry reaches the dock as an indented child under its fold host, or under one of the Help modules, and nothing appears twice.

The rows therefore match the Home screen: modules that belong to Help are listed under Help, and a module folded into a host is nested under that host instead of being listed at the top level.

Measured before the change: 36 top-level dock rows against 19 Home tiles, and NINE keys drawn twice – convert, external_masks, investigate_hit, layer_viewer, lineage, plate_view, profiler, tabulate and train_compare each had a top-level row AND an indented row under the host they fold into. A module that appears in two places in the same column is a module the reader cannot learn the location of.

tiled_apps() is the authority rather than a second hand-written list, so the dock cannot drift from Home again: promoting a module to a tile gives it a dock row, and folding one takes its dock row away, both without touching this function. That is the same lesson _HELP_MODULES already encodes below – 330’s note records one bug from a duplicated list and the retired-section map records another.

Help is not a section (see SECTION_HELP). A section is Home’s categorisation and every Help module is tileless, so Help would be a tab with nothing on it. The dock has no such constraint.

The dock walks these rows in order and starts a new heading whenever the section changes, so rows of one section must be adjacent: a row out of place draws its heading a second time.

spacr.qt.app.folded_children() → Dict[str, Tuple[str, ...]][source]

host key -> the module keys folded onto its masthead.

One mapping, read from the hosts themselves, so the dock, the menu bar and the fold strips use the same assignment. Each host declares its own FOLDED_APPS; this function only collects those tuples.

The dock and the spaCR menu both draw the nested structure from this, showing a folded module one level below the host it belongs to.

Never raises: a host whose module cannot be imported contributes nothing, because a navigation aid must not be able to stop the window being built.

spacr.qt.app.home_bands(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, List[Tuple[str, str, str, str]]]][source]

(band, rows) for the Home tab, in SECTIONS order.

The same grouping the tabs use — every app once, under what it is about. Home is the “all of it” view and each later tab is a filter of it, which is only true while both read this one table.

Bands with no apps are dropped rather than drawn empty. None are, today; the guard is what stops a heading appearing over nothing the day a section’s last app is retired.

spacr.qt.app.home_categories(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, List[str]]][source]

(section, [app key]) for every tab after Home, in tab order.

Computed rather than written down, so a section cannot acquire a tab it has no apps for or lose one it does.

spacr.qt.app.home_stages() → dict[source]

app key → stage, for every app in APPS.

What make_home_page() hands the Home page, so the tiles and the legend colour from the same table this module owns and the widget still knows nothing about what a stage means.

spacr.qt.app.install_the_dialog_filters(app) → tuple[str, ...][source]

Install the registered dialog event filters on a Qt application.

Installers are idempotent and isolated: a failure in one filter does not prevent the remaining filters from being installed.

Parameters:

app – the Qt application each filter installer is called with.

Returns:

Names of the filters installed successfully.

spacr.qt.app.install_the_spaceout_fractal(screen) → bool[source]

Put the spaceout fractal behind screen, if this is spaceout.

Parameters:

screen – the widget the fractal is parented to and sized to fill; an event filter on it keeps the fractal following it.

Returns:

True when it was installed, so the caller knows to skip the ordinary ambient backdrop. False in every normal launch – which is what keeps the mode hidden – and false rather than raising when the fractal cannot be built, so a machine that cannot draw it still gets the animation it always had.

The settings come from Preferences, which shows them only under spaceout, so one place decides both what is offered and what is drawn.

spacr.qt.app.launch(argv: list[str] | None = None) → int[source]

Bootstrap QApplication and show the main window.

spacr.qt.app.make_home_page(parent=None)[source]

Build the Home page exactly as the running app builds it.

The grouping, the stages, the notes and the icon provider are four arguments that have to agree, and a test that assembles its own HomePage is testing a page that does not ship — which is the exact class of bug #16i was: Home grouped by one table and the tabs by another. One constructor call, used by MainWindow and by the suite.

spacr.qt.app.register_app(key: str, name: str, desc: str, section: str, *, factory=None, stage: str | None = None, title: str | None = None, intro: str | None = None, cli_note: str | None = None, api_module: str | None = None, entry: str | None = None, defaults_module: str | None = None, translations: Tuple[str, ...] | None = None) → Tuple[str, str, str, str][source]

Add one app to the registry. The seam a new module registers through.

Call it at import time from the module that owns the app:

from spacr.qt.app import register_app, SECTION_EXPLORE, STAGE_ALPHA

register_app("graph_builder", "Graph Builder",
             "Drag columns onto x / y / colour / facet",
             SECTION_EXPLORE, factory=make_screen, stage=STAGE_ALPHA)

The first four arguments put a tile on Home and a row in the sidebar. The keyword arguments after stage are what make that tile a working app: they are the app’s strings, given ONCE here, and fanned out into the tables that used to need a hand-edit each — see APP_META.

Parameters:
  • key – stable app id. Load-bearing — bridge, cli, validate, the drag-and-drop handlers, settings_model and saved user state all key off it, so it is chosen once and never renamed. Must be unique across built-ins and plugins.

  • name – display name; the sidebar row, tile and menu entry.

  • desc – one-line summary; the tooltip and status tip.

  • section – one of SECTION_ORDER. A section with no apps has no tab, so registering the first app into a new section is what makes that section appear.

  • factory – optional zero-argument callable returning the app’s QWidget screen. It may declare app_key and/or host keyword parameters — host is the MainWindow, for a screen that has signals to connect — and is given whichever of the two it accepts. Omit it and the app gets the generic settings-driven AppScreen, like every pipeline module.

  • stage – optional STAGES member. If omitted, no explicit stage entry is written. Missing entries read as stable until spacr.qt.maturity.apply() assigns alpha to unassessed apps without a declaration. Already affirmed stable apps remain stable. Omitting or deleting an entry is not a stability assessment.

  • title – header shown at the top of the app’s own screen. Defaults to name; give it only when the screen wants the longer form (“Illumination Correction” over a tile that reads “Illumination”). Reaches app_screen.APP_TITLES.

  • intro – the paragraph beside that header — what the module does, in a sentence or two. Defaults to desc. Reaches app_screen.APP_INTROS.

  • cli_note – for an app with NO headless path: one sentence saying so and what to do instead. Reaches cli.INTERACTIVE_ONLY, which is what spacr-run <key> prints instead of “unknown module”. Mutually exclusive with entry in spirit — an app is one or the other.

  • api_module – dotted-or-slashed module path under the generated API docs (“qt/layer_viewer”), for the ⓘ link beside the settings. Reaches settings_model._APP_API_MODULE.

  • entry – "module:function" of the callable the Run button runs. Resolved lazily by registered_entry() and consulted by spacr.qt.bridge.resolve_pipeline_entry(); without it the Run button answers “Not runnable”.

  • defaults_module – the module whose import calls spacr.settings.register_defaults() for this key. settings_model.resolve_default_settings imports it before asking whether the key has registered defaults — otherwise a module that registers its settings at import has no settings panel until something else happens to import it, and the app opens on an empty form. Imported on demand, so registering an app costs no numpy/pandas/torch at startup.

  • translations – the display name in the nine non-English UI languages, in spacr.qt.i18n.LANGUAGES order. Reaches i18n._ROWS and its catalogs.

Returns:

the row that was appended, so a caller can keep it.

Raises:
  • ValueError – on a duplicate key, an unknown section, an unknown stage or an empty name/description.

  • TypeError – if factory is not callable.

spacr.qt.app.registered_entry(key: str)[source]

Import and return the pipeline callable app key registered.

None when the app registered no entry=, which is what an interactive-only app (its own screen, no Run button) does.

spacr.qt.bridge.resolve_pipeline_entry() consults this after its own built-in chain, so register_app(..., entry="mod:func") is all a new pipeline app needs for its Run button to run something. Imported here, on demand, rather than at registration: registering an app must not drag numpy, torch or pandas into a process that only wanted to draw a sidebar.

Parameters:

key – the app key whose entry metadata ('module:function') is imported.

spacr.qt.app.registered_factory(key: str)[source]

The factory registered for key, or None.

A declared app is registered with a LazyScreenFactory standing in for the real callable, and this is where the stand-in ends: asking for the factory imports the screen and returns the module’s own function.

Code downstream of this function therefore never sees the stand-in. _call_screen_factory() decides whether to pass app_key and host by reading the factory’s signature, and a stand-in’s signature is its own, not the real one’s; a test that asserts the registered factory is the module’s function would likewise be comparing against the wrapper. Resolving here means neither ever sees one.

The resolved callable replaces the stand-in in APP_FACTORIES, so the import happens once even if the screen is opened and closed all afternoon. A stand-in whose module fails to import is left in place and None is returned: the app falls back to the generic settings screen rather than taking the window down.

Parameters:

key – the app key looked up in APP_FACTORIES.

spacr.qt.app.registered_metadata(field: str) → dict[source]

{app key: value} for one APP_META field, empty ones dropped.

The pull half of the seam. A side table calls this at the end of its own import to absorb every app that registered before it existed:

_app = sys.modules.get("spacr.qt.app")
if _app is not None:
    for _key, _value in _app.registered_metadata("title").items():
        APP_TITLES.setdefault(_key, _value)

setdefault, not assignment: a table’s own hand-written entry is the more specific one and wins.

Parameters:

field – the APP_META field to collect, such as 'title'.

spacr.qt.app.section_members(section: str, apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, str, str, str]][source]

The APPS rows a category’s tab shows, in registry order.

Apps without a tile are not members. The result is what the tab draws and what its label counts, so a module with no tile is left out of both; otherwise the label would read “Results & QC (7)” over three tiles.

An explicit apps list is filtered too. A caller passing its own rows is asking “which of these belong to this section”, and a tileless one does not belong to any tab whichever list it arrives in.

Parameters:

section – the category name, compared with the fourth element of each APPS row.

spacr.qt.app.tile_sort_key(row: Tuple[str, str, str, str]) → Tuple[int, int][source]

Sort key placing row where SECTION_TILE_ORDER says.

Parameters:

row – an APPS row.

Returns:

(section index, position in that section). An unlisted key sorts after every listed one, keeping registry order among the unlisted by virtue of Python’s stable sort.

spacr.qt.app.tiled_apps(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, str, str, str]][source]

The APPS rows that get a TILE, in registry order.

APPS lists every registered app; this lists the apps Home draws a tile for. The two differ because modules fold into hosts – a folded module keeps its registry row (it still has a screen, an icon, a section and a key to navigate to) and loses only its tile, because it is now reached from a button on its host or from the Help menu.

Anything asking “what does the user see on Home”, “how many tiles are in this band”, or “does this tile’s label fit” wants this. Anything asking “can this key be navigated to”, “does this app have artwork”, or “does every app have a screen” wants APPS – a folded module must still answer yes to all three.

An app registered with the alpha gate has no tile while Preferences > Show alpha features is off: Home does not draw it, so nothing that counts or lists Home’s tiles may count it either.

spacr.qt.app.unregister_app(key: str) → bool[source]

Remove app key from the registry. True if there was one.

The counterpart to register_app(), for a plugin that unloads and for tests that must not leak a registration into the next one — a stray row in APPS is a stray tile, a stray sidebar entry and a stray Ctrl+N binding for every test that follows.

Parameters:

key – the app key to remove, converted to a string.

spacr.qt.app.visible_apps() → List[Tuple[str, str, str, str]][source]

Registry rows allowed by the Alpha/Beta visibility preferences.

Nested helpers

MainWindow.__init__._finish_installer_onboarding()

Show installer consent, optional cache review, report notice and tour.

Background cache inspection finishes before the choices open. The tour waits for the anonymised-report notice to close.

spacr/qt/app.py:3314

MainWindow._on_cache_cleanup_preview_ready.apply()

Revalidate and apply only the explicit current-dialog selection.

Returns:

Action outcome with a refusal reason or removed counts and any private retained paths.

spacr/qt/app.py:6050

MainWindow._retry_screen_backdrop.again() → None

Try the backdrop once more, if the screen is still alive.

spacr/qt/app.py:7909

MainWindow._review_installation_caches.guard(paths)

Hold the application’s run lock and refuse registered active work.

Parameters:

paths – Independently derived candidate cache directories.

Yields:

None while application-local cleanup is allowed, or the existing cache refusal. This does not prove other processes idle.

spacr/qt/app.py:5943

MainWindow._review_installation_caches.prepare()

Inspect the fresh operation’s caches inside the background worker.

Returns:

Immutable preview with the live application-local run guard.

spacr/qt/app.py:5962

MainWindow._show_about._line(html, size, *, muted=False, weight=400, gap=0)

One centred line of the About box, at the given size.

spacr/qt/app.py:4807

MainWindow._start_a_sample_project.open_it(key)

Open the actual module, including its page inside a host screen.

spacr/qt/app.py:7062

_collect_paint_diagnostics.failed(what)

Log the exception being handled and name it in errors.

spacr/qt/app.py:2639

_declared_folds._as_string(node, _depth=0)

The string an AST node stands for, or None if it is not one.

Resolves a literal, a module constant, and another module’s constant read the same way rather than by importing it. _depth bounds that indirection so a constant defined in terms of itself stops instead of recursing.

spacr/qt/app.py:1854

_import_the_data_libraries_off_the_gui_thread._work() → None

Import each library, logging rather than raising a failure.

spacr/qt/app.py:598

_start_icon_prewarm.warm()

Fill the icon caches, and never let a bad file reach the GUI.

spacr/qt/app.py:8710

_start_settings_prewarm._warm_the_settings_imports()

Import each module, logging rather than raising a failure.

spacr/qt/app.py:8542

_start_settings_prewarm.warm()

Import the remaining settings dependencies without building widgets.

spacr/qt/app.py:8536

launch._drain_ai()

Stop every job runner before Qt starts destroying widgets.

Connected to aboutToQuit, which fires however the application exits. See the comment below for why this covers every runner and not only the consoles.

spacr/qt/app.py:8973