spacr.qt.widgets.preview_controls

Shared source-selector controls for every live-preview panel.

Four modules ship a live preview — Mask, Measure, Timelapse and Motility — and each of them opens exactly one source at a time. Before this module they each had a single Choose … button and no way to move to the next field of view or to look at a different channel without re-opening the file dialog.

This module supplies the two dropdowns those panels now share, plus the flat “text only” look they wear:

  • FlatComboBox / FlatButton / FlatSpinBox — chrome-free controls that read like the Live toggle (AiToggleLabel): the theme’s foreground colour, 600 weight, body font size, transparent background, no border, pointing-hand cursor. The accent colour on hover is the only affordance, exactly like the toggles they sit beside.

  • populate_channel_combo() — fills a channel dropdown from a channel count, with an “All channels” entry first.

  • sibling_sources() — lists the other fields of view that live beside the currently-loaded one.

  • enumerate_image_sets() / sample_image_sets() — the sampled source list, described below.

  • install_preview_scale() — each preview’s own scale slider, from spacr.qt.widgets.preview_scale and re-exported here so every shared preview control is importable from one place.

The palette is resolved through spacr.qt.theme.active_palette() at build time and again on every showEvent, so a theme switch made in Preferences lands the next time the panel is shown rather than baking the dark palette’s white text onto a light page. That is the failure spacr.qt.widgets.ai_toggle_label documents.

Why the source list is a sample

sibling_sources() lists every comparable file in the folder, and the panels fed that straight into their field-of-view dropdown. Measured on a 384-well plate at 16 fields and 4 channels (24 576 files) and on one four times larger (98 304 files):

measurement

24k before

24k after

98k before

98k after

folder dropped → panel usable

279 ms

139 ms

1280 ms

579 ms

opening the sets dropdown

175 ms

2 ms

689 ms

2 ms

every change of field

270 ms

1 ms

1233 ms

3 ms

entries in the dropdown

24 576

20

98 304

20

resident memory it held

16.2 MB

3.1 MB

56.0 MB

39.4 MB

image files opened

1

1

1

1

The third row is the one users feel: the panels rebuild their selectors on every load, so re-listing and re-populating the whole plate was paid again on each step through it. It was also the wrong list — four of those entries are the same field of view, once per channel.

enumerate_image_sets() replaces it. It reads file names only, via os.scandir and the project’s own acquisition regex (spacr.utils._get_regex), and groups them into ImageSet records keyed by (plate, well, field) with the channel files hanging off each. No image is opened, decoded or stacked to build that list. sample_image_sets() then draws a bounded random sample — 20 sets by default, adjustable from the FlatSpinBox that sits immediately left of the sets dropdown.

The sample is random but reproducible: the seed is a stable digest of "<folder name>|<total sets>|<cap>|<nonce>" (sample_seed()), so the same plate at the same cap yields the same sets in this session, the next session, and on another machine or mount point — a preview can be described and returned to. It is deliberately not seeded from the clock or from hash(), which is salted per process. Re-rendering never re-draws; only an explicit act does — changing the cap (which is in the seed) or calling reshuffle() (which bumps the nonce).

The sample is drawn across the whole enumeration and then re-sorted into plate order, so the dropdown still reads A01 → P24 while its membership spans the plate rather than being the first N names alphabetically (which on a plate-ordered folder means “all of row A”).

Attributes

Classes

FlatButton

Text-only push button styled like the Live toggle.

FlatComboBox

Text-only dropdown styled like the Live toggle.

FlatSpinBox

Text-only integer box styled like the Live toggle.

ImageSet

One field of view: every channel file that shares a (plate, well, field).

ImageSetSampler

Caches one folder's enumeration and hands out samples of it.

Functions

apply_sample_to_combo(→ str)

Point a sets dropdown at the sampler's current sample.

channel_labels(→ List[str])

Return the entries a channel dropdown shows for n_channels.

channel_view(image, channel)

Return image reduced to channel, or unchanged.

configure_max_sets_box(→ int)

Point a FlatSpinBox at a freshly enumerated folder.

enumerate_image_sets(→ Tuple[List[ImageSet], List[str]])

Group a folder's file names into image sets. Opens nothing.

populate_channel_combo(→ None)

Refill combo with n_channels entries, preserving the selection.

populate_fov_combo(→ None)

Refill an FOV dropdown with sources, selecting current.

sample_image_sets(→ List)

Draw at most max_sets entries from sets, spread across it.

sample_seed(→ int)

The reproducible seed a sampled preview is drawn with.

selected_channel(→ Optional[int])

Return the channel index a channel dropdown selects, or None.

sets_from_paths(→ List[ImageSet])

Wrap an already-listed set of sources as one ImageSet each.

sibling_sources(→ List[pathlib.Path])

List every comparable source sitting beside path.

Module Contents

class spacr.qt.widgets.preview_controls.FlatButton(text: str = '', parent=None, tooltip: str = '')[source]

Bases: _FlatStyleMixin, PySide6.QtWidgets.QPushButton

Text-only push button styled like the Live toggle.

Parameters:
  • text – the caption.

  • parent – parent widget.

  • tooltip – hover text. An empty string leaves the button with no tooltip rather than an empty one, which would otherwise show as a blank box on hover.

Build a flat button.

Parameters:
  • text – the label.

  • parent – parent widget, or None.

  • tooltip – hover text.

class spacr.qt.widgets.preview_controls.FlatComboBox(parent=None, tooltip: str = '')[source]

Bases: _FlatStyleMixin, PySide6.QtWidgets.QComboBox

Text-only dropdown styled like the Live toggle.

Parameters:
  • parent – owning widget.

  • tooltip – hover help; these controls carry no visible label, so the tooltip is the only place their meaning is written down.

Build a flat combo box whose entries are data, not prose.

The language pass is kept off the items deliberately: they are file names and channel indices, and letting them be rewritten breaks every lookup that reads currentText() back – the trap that silently reverted the live preview’s outline colour to its default.

Parameters:
  • parent – parent widget, or None.

  • tooltip – hover text.

class spacr.qt.widgets.preview_controls.FlatSpinBox(parent=None, tooltip: str = '', value: int = 20)[source]

Bases: _FlatStyleMixin, PySide6.QtWidgets.QSpinBox

Text-only integer box styled like the Live toggle.

Used for the “how many image sets may the preview load” cap that sits immediately left of the sets dropdown. The count of sets actually found is carried in the box’s suffix, so the control states 20 of 24576 sets in one place and a sampled preview can never be mistaken for the whole plate.

Parameters:
  • parent – parent widget.

  • tooltip – hover text.

  • value – the cap to open on. The minimum is 1, and the maximum is left wide open until a folder has been enumerated – configure_max_sets_box then clamps it to the number of sets that exist, so the box cannot ask for more than there are.

Build a flat spin box.

The maximum starts wide open and is clamped once a folder has been enumerated – until then there is no honest ceiling to impose.

Parameters:
  • parent – parent widget, or None.

  • tooltip – hover text.

  • value – starting value.

class spacr.qt.widgets.preview_controls.ImageSet[source]

One field of view: every channel file that shares a (plate, well, field).

Built from file names alone. Nothing here has been opened or decoded — channels maps a channel ID to a file name, and it is up to the panel to decide which single file it wants to read.

Parameters:
  • key – (plateID, wellID, fieldID) as the acquisition regex reports them, or ("", "", <file name>) for a name it does not understand.

  • directory – folder the files live in.

  • channels – {channel ID: file name}, one representative file per channel.

  • planes – {channel ID: [file name, ...]}, every plane in acquisition order.

path(channel: str | None = None) → pathlib.Path[source]

Full path to one channel’s file — the lowest channel by default.

plane_paths(channel: str | None = None) → list[source]

Every plane of one channel, in acquisition order.

Falls back to the single path() for data that has no planes recorded, so a caller can always iterate this and get something.

property label: str[source]

Short human label for the dropdown, e.g. A01 f003 (4ch).

property z_count: int[source]

Planes per channel — 1 for flat data, >1 for a z-stack.

Reported rather than assumed: the preview says what it found instead of silently collapsing it.

class spacr.qt.widgets.preview_controls.ImageSetSampler(max_sets: int = DEFAULT_MAX_SETS)[source]

Caches one folder’s enumeration and hands out samples of it.

The panels rebuild their selectors on every image they load. Enumerating the folder each time is what made stepping through a large plate cost 292 ms a step, so the enumeration is done once per folder and every later call reuses it. Only enumerate() touches the filesystem.

Re-sampling is likewise deliberate: sample() is a pure function of (folder, total, cap, nonce), so re-rendering after any settings change returns the identical sets. The sample changes only when the user changes the cap or calls reshuffle().

Parameters:

max_sets – how many image sets a preview may load at once. It is the cap in the sampling described above, so changing it changes the sample – which is why it is a constructor argument rather than something read per render.

Create the sampler that hands out a bounded slice of a plate.

Parameters:

max_sets – how many image sets to offer at most. The dropdown never lists a whole plate, so the sample is bounded and – being seeded from the folder – reproducible.

adopt(directory, sets: Sequence[ImageSet], channels: Sequence[str], metadata_type: str = DEFAULT_METADATA_TYPE, custom_regex: str | None = None) → None[source]

Install an enumeration produced elsewhere — e.g. on a worker thread.

The dialect the caller grouped with belongs in the cache key, or the very next enumerate() — the panels run one on every load — misses and re-scans the whole plate on the GUI thread.

Parameters:
  • directory – the folder the enumeration is of; it becomes the cached folder.

  • sets – the image sets enumerated from it.

  • channels – the channel IDs found across the folder.

  • metadata_type – naming dialect the sets were grouped with.

  • custom_regex – pattern body when metadata_type='custom'.

describe(shown: int) → str[source]

One sentence saying the preview is a sample, and of what.

shown can exceed the cap by one when sample() had to keep a loaded field that the draw missed; that extra entry is called out rather than quietly inflating the reported sample size.

Parameters:

shown – how many sets the dropdown lists.

enumerate(directory, suffixes: Sequence[str], metadata_type: str = DEFAULT_METADATA_TYPE, custom_regex: str | None = None, force: bool = False) → List[ImageSet][source]

Enumerate directory unless it is already the cached one.

The cache key includes the naming dialect: keying on the folder alone meant that confirming a different regex re-used the grouping built with the old one, so the fix appeared to do nothing until the user opened a different folder.

Parameters:
  • directory – folder to enumerate.

  • suffixes – lower-case suffixes that count as a source, as enumerate_image_sets() takes them.

  • metadata_type – naming dialect, see spacr.utils._get_regex().

  • custom_regex – pattern body when metadata_type='custom'.

  • force – re-scan even when the cache key matches.

enumerate_paths(directory, lister, force: bool = False) → List[ImageSet][source]

Cache a caller-supplied listing of whole sources as one set each.

For panels whose field of view is a folder of frames or a stacked array rather than a group of per-channel files. lister is only called when the folder is not the cached one, which is what keeps stepping through fields free.

Parameters:
  • directory – the folder the listing belongs to; its string form is the cache key.

  • lister – a no-argument callable returning the source paths, each wrapped by sets_from_paths().

  • force – call lister even when the folder is cached.

invalidate() → None[source]

Forget the cache, so the next enumerate() really scans.

pin(item: ImageSet | None) → None[source]

Keep item in the list even when the draw missed it.

A user who drops one specific file on the panel must find it in the panel’s own dropdown. The pin is sticky: it survives navigating away to a sampled field, so the file they opened stays reachable and the entry list does not shift under them while they browse. Redrawing the sample — the only thing that is allowed to change the list — clears it.

Parameters:

item – the set to pin; None or a set not in the current enumeration leaves the pin unchanged.

reshuffle() → None[source]

Explicitly draw a different sample of the same folder.

sample(keep: ImageSet | None = None) → List[ImageSet][source]

The sets to show: the draw, plus any pinned set.

keep pins as a side effect, so callers can pass whatever is loaded without tracking the pin themselves.

set_for_path(path) → ImageSet | None[source]

The enumerated set a given file belongs to, if any.

Indexed by file name on first use rather than scanned. This runs twice on every image load, and a linear scan of a 24 576-set plate put ~10 ms back onto each change of field — most of what the sampling had just taken off.

Parameters:

path – a file path (str or path-like) or None; only its file name is looked up among the sets’ channel files.

set_max(max_sets: int) → bool[source]

Change the cap. Returns True when it actually changed.

Parameters:

max_sets – the new cap, converted with int; zero or less means no cap. A change clears the pin.

property channels: List[str][source]

Channel IDs the enumeration found across the folder.

property directory: str | None[source]

The folder this sampler draws its images from.

Returns:

the directory path.

property seed: int[source]

The seed the current sample is drawn with.

property sets: List[ImageSet][source]

Every set the folder holds — the population, not the sample.

property total: int[source]

How many sets the folder holds, not how many are shown.

spacr.qt.widgets.preview_controls.apply_sample_to_combo(combo: PySide6.QtWidgets.QComboBox, box: PySide6.QtWidgets.QSpinBox | None, sampler: ImageSetSampler, current_path, tooltip: str = '') → str[source]

Point a sets dropdown at the sampler’s current sample.

Configures the cap box, draws the sample (keeping whatever is loaded), and refills the dropdown. Touches no file: the sampler must already have been enumerated.

Parameters:
  • combo – the sets dropdown to refill.

  • box – the cap spin box, or None to leave the sampler’s cap as it is.

  • sampler – an already-enumerated ImageSetSampler.

  • current_path – the file loaded now (or None); its set is kept in the sample and selected.

  • tooltip – text placed before the sample sentence in the dropdown’s tooltip.

Returns:

the sentence stating what fraction of the folder is on show.

spacr.qt.widgets.preview_controls.channel_labels(n_channels: int, include_all: bool = True) → List[str][source]

Return the entries a channel dropdown shows for n_channels.

Parameters:
  • n_channels – number of channels; Ch 0 up to Ch n-1 are listed (a negative count lists none).

  • include_all – put ALL_CHANNELS first.

spacr.qt.widgets.preview_controls.channel_view(image, channel: int | None)[source]

Return image reduced to channel, or unchanged.

Out-of-range indices and 2-D images fall through untouched — a stale selection must never raise while the user is loading a new field.

Parameters:
  • image – an array of shape (H, W, C), or anything else (returned as is); None is returned unchanged.

  • channel – index into the last axis, or None for all channels.

spacr.qt.widgets.preview_controls.configure_max_sets_box(box: PySide6.QtWidgets.QSpinBox, total: int) → int[source]

Point a FlatSpinBox at a freshly enumerated folder.

The suffix carries the total, so the control reads 20 of 24576 sets. The maximum is clamped to the total so it can never read 50 of 12 — a cap above what exists is not a real cap.

Parameters:
  • box – the cap spin box; its suffix, maximum and enabled state are set with signals blocked, and it is enabled only for more than one set.

  • total – number of image sets in the folder; negative counts as 0.

Returns:

the cap the box now holds, which the clamp may have lowered. Callers must feed it back to the sampler, or a folder small enough to clamp would leave the box saying 12 while the dropdown showed 50.

spacr.qt.widgets.preview_controls.enumerate_image_sets(directory, suffixes: Sequence[str], metadata_type: str = DEFAULT_METADATA_TYPE, custom_regex: str | None = None) → Tuple[List[ImageSet], List[str]][source]

Group a folder’s file names into image sets. Opens nothing.

Reads the directory with os.scandir() — on Linux that answers “is this a file?” straight out of the dirent, so no stat is issued per entry — and matches each name against _get_regex(). Names the regex understands are grouped by (plateID, wellID, fieldID); names it does not become one set each, so an ad-hoc folder of a.tif/b.tif still lists exactly as it always did. Names that start with a dot are skipped, as a run skips them: the ._<name> sidecars macOS writes on exFAT and network volumes end in .tif too and hold no image.

Parameters:
  • directory – folder to enumerate.

  • suffixes – lower-case suffixes that count as a source.

  • metadata_type – naming dialect, see spacr.utils._get_regex().

  • custom_regex – pattern body when metadata_type='custom'.

Returns:

(sets sorted by key, channel IDs found across the folder).

spacr.qt.widgets.preview_controls.populate_channel_combo(combo: PySide6.QtWidgets.QComboBox, n_channels: int, include_all: bool = True, keep: str | None = None) → None[source]

Refill combo with n_channels entries, preserving the selection.

Parameters:
  • combo – the dropdown to refill.

  • n_channels – how many channels the loaded source holds.

  • include_all – prepend the ALL_CHANNELS entry.

  • keep – entry to re-select; defaults to what is selected now.

spacr.qt.widgets.preview_controls.populate_fov_combo(combo: PySide6.QtWidgets.QComboBox, sources: Sequence[pathlib.Path], current=None, labels: Sequence[str] | None = None) → None[source]

Refill an FOV dropdown with sources, selecting current.

Each entry stores its full path as item data, so the caller never has to reconstruct a path from the (deliberately short) visible label.

Parameters:
  • combo – field-of-view dropdown to clear and refill while its signals are temporarily blocked.

  • sources – ordered source paths; each becomes one item whose data is the full string path.

  • labels – visible text per entry; defaults to each path’s file name. Set-based enumeration passes A01 f003 style labels so the entry names the field of view rather than one of its channel files.

spacr.qt.widgets.preview_controls.sample_image_sets(sets: Sequence, max_sets: int, seed: int) → List[source]

Draw at most max_sets entries from sets, spread across it.

The draw is random — so the sample represents the whole plate rather than the first N names, which on a plate-ordered folder is all of row A — but the winners are then restored to their original order, so the dropdown still reads front to back.

max_sets of zero or less means “no cap”.

Parameters:
  • sets – the entries to draw from, in display order.

  • max_sets – the most entries to return.

  • seed – seed for random.Random, so the same seed draws the same positions.

spacr.qt.widgets.preview_controls.sample_seed(directory, total: int, max_sets: int, nonce: int = 0) → int[source]

The reproducible seed a sampled preview is drawn with.

Digest of "<folder name>|<total sets>|<cap>|<nonce>". Stable across processes and machines — unlike hash(), which is salted per interpreter — so a user can name the plate and the cap and get the same sets back.

Deliberately the folder’s name, not its full path: the same plate read from a local copy and from the NAS it was acquired on must preview the same fields, or “the sample I looked at” is not a thing anyone can hand over. Two unrelated folders sharing a name draw the same positions, which selects different sets because their contents differ.

Parameters:
  • directory – the folder being sampled; only its name is used.

  • total – number of image sets in it.

  • max_sets – the sample cap.

  • nonce – a counter that changes the seed for a re-draw.

spacr.qt.widgets.preview_controls.selected_channel(combo: PySide6.QtWidgets.QComboBox) → int | None[source]

Return the channel index a channel dropdown selects, or None.

None means ALL_CHANNELS (or an empty dropdown) — show the source exactly as it is stored.

Parameters:

combo – a channel dropdown filled from channel_labels(); a Ch <n> entry gives n, anything else None.

spacr.qt.widgets.preview_controls.sets_from_paths(paths: Sequence[pathlib.Path]) → List[ImageSet][source]

Wrap an already-listed set of sources as one ImageSet each.

The Timelapse and Motility previews’ sources are whole folders of frames or stacked arrays — one source already is one field of view, so there is nothing to group. They still want the cap and the reproducible draw, so they feed their own listing through here and share the sampler.

Parameters:

paths – the sources (folders or files, str or path-like); each becomes a set keyed ("", "", <name>) in its parent folder.

spacr.qt.widgets.preview_controls.sibling_sources(path, suffixes: Sequence[str], directories: bool = False) → List[pathlib.Path][source]

List every comparable source sitting beside path.

Names that start with a dot are left out, as a Mask run leaves them out: on exFAT, FAT and many network shares macOS writes a ._<name> sidecar beside every file, with the same ending and no image in it.

Parameters:
  • path – the currently-loaded file (or folder).

  • suffixes – lower-case suffixes that count as a source.

  • directories – when True, list sibling folders instead of files — the Timelapse preview’s fields of view are folders of frames.

Returns:

sorted paths, always including path itself when it exists.

spacr.qt.widgets.preview_controls.MAX_SETS_TOOLTIP = Multiline-String[source]
Show Value
"""Maximum number of image sets loaded by the preview.

For large experiments, filenames are grouped into image sets comprising one field of view and all of its channels. The specified number of sets is sampled across the plate; files outside the sample are not opened.

Sampling is deterministic: the same folder and maximum produce the same sets. Changing the maximum selects a new sample, whereas re-rendering with the same settings preserves the selection."""