spacr.qt.dnd

Drag-and-drop system for AppScreens.

Design:

  • DropHandler — per-module policy: what folders/files this screen accepts, how to fix a “close-but-not-quite” drop, and what to do once a drop is accepted.

  • install_dropzone() — attaches Qt drop event handlers to any widget (usually the AppScreen itself) and wires them to a DropHandler.

  • suggest_alternatives_dialog() — the “did you mean X?” chooser shown when the dropped folder can’t be used as-is but a sibling / child folder can.

Behaviour common to every module:

  • Dropping a *.csv file → treat as a settings CSV and call the screen’s apply_settings_dict (imports settings, doesn’t overwrite the source folder).

  • Dropping a folder → hand off to the module’s DropHandler. If it’s a good fit, the handler calls screen._set_src (or equivalent). If it’s a near-miss the user gets the “did you mean” dialog.

  • Dropping multiple folders → the handler is called once per folder in the order the OS delivers them. Modules that don’t handle multi-drop degrade to first-only.

Where the work happens

A drop is delivered by Qt on the GUI thread and every path in it is a path the USER chose – which may sit on a sleeping autofs share that takes more than twenty seconds to answer one stat (see spacr.qt.path_probe). So the drop is split in two, and the seam is the rule for anything added here:

  • _classify_drop() asks the disk everything the drop needs to know, on the screen’s drop scanner. No Qt, no widgets, plain data out.

  • _deliver_drop() acts on that data on the GUI thread: the settings import, handler.apply, the rejection report, the “did you mean” dialog.

_route_drop() joins them, and keeps concurrent drops on one screen in the order the user made them.

Per-module policies live in spacr.qt.dnd_handlers.

Classes

DropHandler

Per-module drop policy.

Functions

choose_one_dialog(→ Optional[str])

Ask which of options was meant. None when nobody answered.

find_image_folders_nearby(→ List[pathlib.Path])

Search parent + immediate children of path for folders that

has_images_in(→ bool)

Return True if path contains at least min_count image

install_dropzone(→ None)

Wire target to accept drops routed through handler.

install_for(→ bool)

Attach app_key's drop policy to target. Never raises.

sample_image_names(→ List[pathlib.Path])

Return up to n image paths from path — used by the

suggest_alternatives_dialog(→ Optional[pathlib.Path])

Modal that lets the user pick from alternatives.

Module Contents

class spacr.qt.dnd.DropHandler[source]

Bases: abc.ABC

Per-module drop policy.

Subclasses implement:

can_accept(path) — is this path good to go? apply(path, screen) — wire it into the screen.

And optionally override:

suggest_alternatives(p) — return nearby folders that DO fit. error_message(p) — return the “why not?” string. accepts_multiple() — True if multi-folder drops make sense.

accepts_multiple() → bool[source]

Return True to be called per-folder on multi-item drops.

abstract apply(path: pathlib.Path, screen) → None[source]

Wire path into screen (set src, populate settings, etc.).

Parameters:
  • path – the dropped folder or file, already accepted by can_accept() (or picked from its alternatives).

  • screen – the app screen that received the drop; the handler writes the path into its settings form.

apply_all(paths: Sequence[pathlib.Path], screen) → bool[source]

Wire every accepted path of ONE drop in at once, in drop order.

A handler whose screen builds one list out of a drop – Make Masks’ queue, Plaque Assay’s selection – overrides this, because apply() called once per path cannot tell the second file of a drop from the first file of the next one. The default declines, and each path goes through apply() as before.

Parameters:
  • paths – the paths can_accept() accepted, in drop order.

  • screen – the screen to wire the drop into.

Returns:

True when the drop was handled here; False sends every path through apply() instead.

abstract can_accept(path: pathlib.Path) → bool[source]

Return True if path (folder OR file) is usable as-is.

Parameters:

path – the dropped folder or file to test.

error_message(path: pathlib.Path) → str[source]

Human-friendly explanation for why path can’t be used.

Parameters:

path – the dropped folder or file that was rejected; the default message names only its final component.

suggest_alternatives(path: pathlib.Path) → List[pathlib.Path][source]

When can_accept returns False, return sibling/child folders that WOULD be accepted so the UI can prompt “did you mean…”.

Default: no suggestions.

Parameters:

path – the dropped folder or file that can_accept() rejected; the default implementation ignores it.

spacr.qt.dnd.choose_one_dialog(parent, headline: str, question: str, options: Sequence[str]) → str | None[source]

Ask which of options was meant. None when nobody answered.

Distinct from suggest_alternatives_dialog(), which says the drop cannot be used. This one is asked when the drop resolved perfectly and landed on more than one right answer — two tables in the database, two masks in masks/ — where “did you mean…” would be telling the user they made a mistake they did not make.

Parameters:
  • parent – the widget to centre the dialog on.

  • headline – what was found, e.g. “plate1.db holds 4 tables.”

  • question – what is being asked, e.g. “Which one should be loaded?”

  • options – the candidates, in the order to offer them.

Returns:

the chosen option, or None when cancelled.

spacr.qt.dnd.find_image_folders_nearby(path: pathlib.Path, max_depth: int = 1, min_count: int = 1) → List[pathlib.Path][source]

Search parent + immediate children of path for folders that contain images. Excludes path itself if it already qualifies.

Handy for the “did you mean X?” prompt when the user drops the wrong sibling of a plate folder. Worker thread only: it lists two levels of a folder the user chose.

Parameters:

path – folder the user dropped; its siblings and, if it is a directory, its immediate child folders are searched.

spacr.qt.dnd.has_images_in(path: pathlib.Path, min_count: int = 1, exts: Sequence[str] = IMAGE_EXTS) → bool[source]

Return True if path contains at least min_count image files at its top level (does not recurse). Worker thread only.

Parameters:

path – folder to inspect; a path that is not a directory yields False.

spacr.qt.dnd.install_dropzone(target: PySide6.QtWidgets.QWidget, handler: DropHandler, screen: PySide6.QtWidgets.QWidget) → None[source]

Wire target to accept drops routed through handler.

Typically called from AppScreen.__init__: target is self and screen is also self. Splitting them lets non-AppScreen widgets install a dropzone that acts on a different owner (e.g. a specific input row).

Parameters:
  • target – the QWidget that receives drag/drop events.

  • handler – the module’s DropHandler policy.

  • screen – the widget passed to handler.apply — usually the AppScreen.

spacr.qt.dnd.install_for(target: PySide6.QtWidgets.QWidget, app_key: str, screen: PySide6.QtWidgets.QWidget = None) → bool[source]

Attach app_key’s drop policy to target. Never raises.

The one line a screen adds to accept drops. Which policy that is comes from spacr.qt.dnd_handlers.get_handler(), so a screen never names a handler class and a screen with no declared policy still gets the source-folder fallback.

Failure is a missing convenience, not a broken screen — a Qt build with no drag-and-drop, or a handler whose import fails, must not stop the screen being constructed. It is logged and the screen goes up without a dropzone.

Parameters:
  • target – the widget that receives the drag/drop events.

  • app_key – the registered app key, e.g. "graph_builder".

  • screen – the object handed to handler.apply; target when omitted.

Returns:

whether the dropzone was installed.

spacr.qt.dnd.sample_image_names(path: pathlib.Path, n: int = 8, exts: Sequence[str] = IMAGE_EXTS) → List[pathlib.Path][source]

Return up to n image paths from path — used by the filename-regex preview in the mask handler. Worker thread only.

Parameters:

path – folder whose top-level image files are listed in sorted order; a path that is not a directory yields an empty list.

spacr.qt.dnd.suggest_alternatives_dialog(parent, original: pathlib.Path, alternatives: Sequence[pathlib.Path], why: str = '') → pathlib.Path | None[source]

Modal that lets the user pick from alternatives.

Parameters:
  • parent – widget that owns the modal dialog.

  • original – the rejected path; its final component is named in the dialog’s heading.

  • alternatives – candidate paths listed for the user, the first one preselected; the chosen entry is returned.

Returns:

the chosen Path, or None if cancelled.

Nested helpers

_route_drop.deliver(report)

Act on what scan found, back on the GUI thread.

Parameters:

report – the classification scan produced.

spacr/qt/dnd.py:665

_route_drop.scan()

Classify the dropped paths, off the GUI thread.

This is the half that touches the filesystem, and a dropped folder is a path the USER chose – which on some machines is an autofs share that takes twenty seconds to answer its first stat.

Returns:

the classification report deliver will act on.

spacr/qt/dnd.py:651