anki/qt/aqt/taskman.py

102 lines
3.3 KiB
Python
Raw Normal View History

2020-01-19 01:05:37 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Helper for running tasks on background threads.
clear_unused_tags and browser redraw improvements - clear_unused_tags() is now undoable, and returns the number of removed notes - add a new mw.query_op() helper for immutable queries - decouple "freeze/unfreeze ui state" hooks from the "interface update required" hook, so that the former is fired even on error, and can be made re-entrant - use a 'block_updates' flag in Python, instead of setUpdatesEnabled(), as the latter has the side-effect of preventing child windows like tooltips from appearing, and forces a full redrawn when updates are enabled again. The new behaviour leads to the card list blanking out when a long-running op is running, but in the future if we cache the cell values we can just display them from the cache instead. - we were indiscriminately saving the note with saveNow(), due to the call to saveTags(). Changed so that it only saves when the tags field is focused. - drain the "on_done" queue on main before launching a new background task, to lower the chances of something in on_done making a small query to the DB and hanging until a long op finishes - the duplicate check in the editor was executed after the webview loads, leading to it hanging until the sidebar finishes loading. Run it at set_note() time instead, so that the editor loads first. - don't throw an error when a long-running op started with with_progress() finishes after the window it was launched from has closed - don't throw an error when the browser is closed before the sidebar has finished loading
2021-03-17 12:27:42 +01:00
See QueryOp() and CollectionOp() for higher-level routines.
2020-01-19 01:05:37 +01:00
"""
from __future__ import annotations
2020-01-19 01:05:37 +01:00
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from threading import Lock
2020-05-04 13:52:56 +02:00
from typing import Any, Callable, Dict, List, Optional
2020-01-19 01:05:37 +01:00
from PyQt5.QtCore import QObject, pyqtSignal
import aqt
from aqt.qt import QWidget, qconnect
Closure = Callable[[], None]
2020-01-19 01:05:37 +01:00
class TaskManager(QObject):
_closures_pending = pyqtSignal()
2020-01-19 01:05:37 +01:00
def __init__(self, mw: aqt.AnkiQt) -> None:
2020-01-19 01:05:37 +01:00
QObject.__init__(self)
self.mw = mw.weakref()
2020-01-19 01:05:37 +01:00
self._executor = ThreadPoolExecutor()
self._closures: List[Closure] = []
self._closures_lock = Lock()
qconnect(self._closures_pending, self._on_closures_pending)
2020-01-19 01:05:37 +01:00
2021-02-01 14:28:21 +01:00
def run_on_main(self, closure: Closure) -> None:
"Run the provided closure on the main thread."
with self._closures_lock:
self._closures.append(closure)
self._closures_pending.emit() # type: ignore
2020-01-19 01:05:37 +01:00
def run_in_background(
2020-01-19 01:05:37 +01:00
self,
task: Callable,
on_done: Optional[Callable[[Future], None]] = None,
2020-01-19 01:05:37 +01:00
args: Optional[Dict[str, Any]] = None,
) -> Future:
"""Use QueryOp()/CollectionOp() in new code.
Run task on a background thread.
If on_done is provided, it will be called on the main thread with
the completed future.
2020-01-19 01:05:37 +01:00
Args if provided will be passed on as keyword arguments to the task callable."""
clear_unused_tags and browser redraw improvements - clear_unused_tags() is now undoable, and returns the number of removed notes - add a new mw.query_op() helper for immutable queries - decouple "freeze/unfreeze ui state" hooks from the "interface update required" hook, so that the former is fired even on error, and can be made re-entrant - use a 'block_updates' flag in Python, instead of setUpdatesEnabled(), as the latter has the side-effect of preventing child windows like tooltips from appearing, and forces a full redrawn when updates are enabled again. The new behaviour leads to the card list blanking out when a long-running op is running, but in the future if we cache the cell values we can just display them from the cache instead. - we were indiscriminately saving the note with saveNow(), due to the call to saveTags(). Changed so that it only saves when the tags field is focused. - drain the "on_done" queue on main before launching a new background task, to lower the chances of something in on_done making a small query to the DB and hanging until a long op finishes - the duplicate check in the editor was executed after the webview loads, leading to it hanging until the sidebar finishes loading. Run it at set_note() time instead, so that the editor loads first. - don't throw an error when a long-running op started with with_progress() finishes after the window it was launched from has closed - don't throw an error when the browser is closed before the sidebar has finished loading
2021-03-17 12:27:42 +01:00
# Before we launch a background task, ensure any pending on_done closure are run on
# main. Qt's signal/slot system will have posted a notification, but it may
# not have been processed yet. The on_done() closures may make small queries
# to the database that we want to run first - if we delay them until after the
# background task starts, and it takes out a long-running lock on the database,
# the UI thread will hang until the end of the op.
self._on_closures_pending()
2020-01-19 01:05:37 +01:00
if args is None:
args = {}
fut = self._executor.submit(task, **args)
if on_done is not None:
fut.add_done_callback(
lambda future: self.run_on_main(lambda: on_done(future))
2020-01-19 01:05:37 +01:00
)
return fut
2020-01-19 01:05:37 +01:00
def with_progress(
self,
task: Callable,
on_done: Optional[Callable[[Future], None]] = None,
parent: Optional[QWidget] = None,
2020-05-30 04:28:22 +02:00
label: Optional[str] = None,
2020-05-31 03:49:05 +02:00
immediate: bool = False,
2021-02-02 14:30:53 +01:00
) -> None:
"Use QueryOp()/CollectionOp() in new code."
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
2021-02-02 14:30:53 +01:00
def wrapped_done(fut: Future) -> None:
self.mw.progress.finish()
if on_done:
on_done(fut)
self.run_in_background(task, wrapped_done)
def _on_closures_pending(self) -> None:
"""Run any pending closures. This runs in the main thread."""
with self._closures_lock:
closures = self._closures
self._closures = []
2020-01-19 01:05:37 +01:00
for closure in closures:
closure()