"""A tiny background-job helper so heavy work doesn't freeze the GUI. The app is otherwise synchronous, but a single ``detector.detect()`` (CPU YOLO) or a DeepMosaics restore can block the GUI thread for seconds — ``processEvents`` only runs *between* frames, not *inside* one heavy call. So detection and restoration run on a ``QThreadPool`` thread via :class:`Job`; results come back to the GUI through queued Qt signals. Contract: the job function ``fn(job)`` runs on a worker thread and may ONLY touch plain data + the engines (no Qt widgets). It reports progress with ``job.progress``/ ``job.tick`` and checks ``job.cancelled`` to stop early. Its return value is delivered on the GUI thread via the ``done`` signal; raising :class:`Cancelled` is reported as a clean cancel (``done`` with ``None``), any other exception via ``failed``. """ from __future__ import annotations from collections.abc import Callable from typing import Any from PySide6.QtCore import QObject, QRunnable, Signal from ..core.restore.base import Cancelled class _Signals(QObject): progress = Signal(int, int, str) # done, total, message tick = Signal(object) # incremental payload (delivered on GUI thread) done = Signal(object) # final result (None if cancelled) failed = Signal(str) # error message class Job(QRunnable): """Runs ``fn(job)`` on a thread pool, marshaling progress/result to the GUI.""" def __init__(self, fn: Callable[[Job], Any]) -> None: super().__init__() self.setAutoDelete(False) # the GUI keeps a reference until `done`/`failed` self.signals = _Signals() self._fn = fn self._cancelled = False # -- called from the GUI thread -- def cancel(self) -> None: self._cancelled = True @property def cancelled(self) -> bool: return self._cancelled # -- called from the worker thread by `fn` -- def progress(self, done: int, total: int, message: str = "") -> None: self.signals.progress.emit(done, total, message) def tick(self, payload: Any) -> None: self.signals.tick.emit(payload) # -- thread entry point -- def run(self) -> None: try: result = self._fn(self) except Cancelled: self.signals.done.emit(None) except Exception as exc: self.signals.failed.emit(str(exc)) else: self.signals.done.emit(result)