Files
HVideoTool/hvideotool/ui/workers.py
T

68 lines
2.4 KiB
Python

"""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)