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
|
|
|
|
2021-05-08 08:56:51 +02:00
|
|
|
See QueryOp() and CollectionOp() for higher-level routines.
|
2020-01-19 01:05:37 +01:00
|
|
|
"""
|
|
|
|
|
2020-05-04 13:30:41 +02: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
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Any, Callable
|
2020-01-19 01:05:37 +01:00
|
|
|
|
|
|
|
from PyQt5.QtCore import QObject, pyqtSignal
|
|
|
|
|
2020-05-04 13:30:41 +02:00
|
|
|
import aqt
|
|
|
|
from aqt.qt import QWidget, qconnect
|
2020-05-04 05:23:08 +02:00
|
|
|
|
2020-01-22 05:09:51 +01:00
|
|
|
Closure = Callable[[], None]
|
2020-01-19 01:05:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
class TaskManager(QObject):
|
2020-01-22 05:09:51 +01:00
|
|
|
_closures_pending = pyqtSignal()
|
2020-01-19 01:05:37 +01:00
|
|
|
|
2020-05-04 13:30:41 +02:00
|
|
|
def __init__(self, mw: aqt.AnkiQt) -> None:
|
2020-01-19 01:05:37 +01:00
|
|
|
QObject.__init__(self)
|
2020-05-04 13:30:41 +02:00
|
|
|
self.mw = mw.weakref()
|
2020-01-19 01:05:37 +01:00
|
|
|
self._executor = ThreadPoolExecutor()
|
2021-10-03 10:59:42 +02:00
|
|
|
self._closures: list[Closure] = []
|
2020-01-22 05:09:51 +01:00
|
|
|
self._closures_lock = Lock()
|
2020-05-04 05:23:08 +02:00
|
|
|
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:
|
2020-01-22 05:09:51 +01:00
|
|
|
"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
|
|
|
|
2020-01-22 05:09:51 +01:00
|
|
|
def run_in_background(
|
2020-01-19 01:05:37 +01:00
|
|
|
self,
|
|
|
|
task: Callable,
|
2021-10-03 10:59:42 +02:00
|
|
|
on_done: Callable[[Future], None] | None = None,
|
|
|
|
args: dict[str, Any] | None = None,
|
2020-01-19 01:05:37 +01:00
|
|
|
) -> Future:
|
2021-05-08 08:56:51 +02:00
|
|
|
"""Use QueryOp()/CollectionOp() in new code.
|
|
|
|
|
|
|
|
Run task on a background thread.
|
2020-01-22 05:09:51 +01:00
|
|
|
|
|
|
|
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:
|
2020-01-22 05:09:51 +01:00
|
|
|
fut.add_done_callback(
|
|
|
|
lambda future: self.run_on_main(lambda: on_done(future))
|
2020-01-19 01:05:37 +01:00
|
|
|
)
|
|
|
|
|
2020-01-22 05:09:51 +01:00
|
|
|
return fut
|
2020-01-19 01:05:37 +01:00
|
|
|
|
2020-05-04 13:30:41 +02:00
|
|
|
def with_progress(
|
|
|
|
self,
|
|
|
|
task: Callable,
|
2021-10-03 10:59:42 +02:00
|
|
|
on_done: Callable[[Future], None] | None = None,
|
|
|
|
parent: QWidget | None = None,
|
|
|
|
label: str | None = None,
|
2020-05-31 03:49:05 +02:00
|
|
|
immediate: bool = False,
|
2021-02-02 14:30:53 +01:00
|
|
|
) -> None:
|
2021-05-08 08:56:51 +02:00
|
|
|
"Use QueryOp()/CollectionOp() in new code."
|
2020-05-31 03:24:33 +02:00
|
|
|
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
|
2020-05-04 13:30:41 +02:00
|
|
|
|
2021-02-02 14:30:53 +01:00
|
|
|
def wrapped_done(fut: Future) -> None:
|
2020-05-04 13:30:41 +02:00
|
|
|
self.mw.progress.finish()
|
2021-01-30 12:09:11 +01:00
|
|
|
if on_done:
|
|
|
|
on_done(fut)
|
2020-05-04 13:30:41 +02:00
|
|
|
|
|
|
|
self.run_in_background(task, wrapped_done)
|
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def _on_closures_pending(self) -> None:
|
2020-01-22 05:09:51 +01:00
|
|
|
"""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
|
|
|
|
2020-01-22 05:09:51 +01:00
|
|
|
for closure in closures:
|
|
|
|
closure()
|