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 aDropHandler.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
*.csvfile → treat as a settings CSV and call the screen’sapply_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 callsscreen._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¶
Per-module drop policy. |
Functions¶
|
Ask which of |
|
Search parent + immediate children of |
|
Return True if |
|
Wire |
|
Attach |
|
Return up to |
|
Modal that lets the user pick from |
Module Contents¶
- class spacr.qt.dnd.DropHandler[source]¶
Bases:
abc.ABCPer-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.
- abstract apply(path: pathlib.Path, screen) None[source]¶
Wire
pathintoscreen(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 throughapply()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
pathcan’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_acceptreturns 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
optionswas meant.Nonewhen 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 inmasks/— 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
pathfor folders that contain images. Excludespathitself 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
pathcontains at leastmin_countimage 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
targetto accept drops routed throughhandler.Typically called from
AppScreen.__init__:targetisselfandscreenis alsoself. 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 totarget. 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;targetwhen 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
nimage paths frompath— 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
scanfound, back on the GUI thread.- Parameters:
report – the classification
scanproduced.
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
autofsshare that takes twenty seconds to answer its first stat.- Returns:
the classification report
deliverwill act on.
spacr/qt/dnd.py:651