move query_op into operations/, and add the ability to show progress

This commit is contained in:
Damien Elmes 2021-05-08 16:20:10 +10:00
parent b887032244
commit f2db822c08
9 changed files with 126 additions and 58 deletions

View File

@ -8,6 +8,7 @@ from typing import List, Sequence
import aqt
from anki.notes import NoteId
from aqt import AnkiQt
from aqt.operations import QueryOp
from aqt.operations.note import find_and_replace
from aqt.operations.tag import find_and_replace_tag
from aqt.qt import *
@ -40,10 +41,11 @@ class FindAndReplaceDialog(QDialog):
self.field_names: List[str] = []
# fetch field names and then show
mw.query_op(
lambda: mw.col.field_names_for_note_ids(note_ids),
QueryOp(
parent=mw,
op=lambda col: col.field_names_for_note_ids(note_ids),
success=self._show,
)
).run_in_background()
def _show(self, field_names: Sequence[str]) -> None:
# add "all fields" and "tags" to the top of the list

View File

@ -20,6 +20,7 @@ from aqt.browser.sidebar.searchbar import SidebarSearchBar
from aqt.browser.sidebar.toolbar import SidebarTool, SidebarToolbar
from aqt.clayout import CardLayout
from aqt.models import Models
from aqt.operations import QueryOp
from aqt.operations.deck import (
remove_decks,
rename_deck,
@ -163,7 +164,9 @@ class SidebarTreeView(QTreeView):
# needs to be set after changing model
qconnect(self.selectionModel().selectionChanged, self._on_selection_changed)
self.mw.query_op(self._root_tree, success=on_done)
QueryOp(
parent=self.browser, op=lambda _: self._root_tree(), success=on_done
).run_in_background()
def restore_current(self, current: SidebarItem) -> None:
if current := self.find_item(current.has_same_id):
@ -892,7 +895,11 @@ class SidebarTreeView(QTreeView):
new_name=full_name,
).run_in_background()
self.mw.query_op(lambda: self.mw.col.get_deck(deck_id), success=after_fetch)
QueryOp(
parent=self.browser,
op=lambda col: col.get_deck(deck_id),
success=after_fetch,
).run_in_background()
def delete_decks(self, _item: SidebarItem) -> None:
remove_decks(parent=self, deck_ids=self._selected_decks()).run_in_background()

View File

@ -12,6 +12,7 @@ from anki.collection import OpChanges
from anki.decks import Deck, DeckCollapseScope, DeckId, DeckTreeNode
from anki.utils import intTime
from aqt import AnkiQt, gui_hooks
from aqt.operations import QueryOp
from aqt.operations.deck import (
add_deck_dialog,
remove_decks,
@ -284,7 +285,9 @@ class DeckBrowser:
parent=self.mw, deck_id=did, new_name=new_name
).run_in_background()
self.mw.query_op(lambda: self.mw.col.get_deck(did), success=prompt)
QueryOp(
parent=self.mw, op=lambda col: col.get_deck(did), success=prompt
).run_in_background()
def _options(self, did: DeckId) -> None:
# select the deck first, because the dyn deck conf assumes the deck

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import aqt
from anki.decks import Deck
from aqt.operations import QueryOp
from aqt.operations.deck import update_deck
from aqt.qt import *
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
@ -22,7 +23,11 @@ class DeckDescriptionDialog(QDialog):
# set on success
self.deck: Deck
mw.query_op(mw.col.decks.get_current, success=self._setup_and_show)
QueryOp(
parent=self.mw,
op=lambda col: col.decks.get_current(),
success=self._setup_and_show,
).run_in_background()
def _setup_and_show(self, deck: Deck) -> None:
if deck.WhichOneof("kind") != "normal":

View File

@ -30,6 +30,7 @@ from anki.httpclient import HttpClient
from anki.notes import DuplicateOrEmptyResult, Note
from anki.utils import checksum, isLin, isWin, namedtmp
from aqt import AnkiQt, colors, gui_hooks
from aqt.operations import QueryOp
from aqt.operations.note import update_note
from aqt.qt import *
from aqt.sound import av_player
@ -496,7 +497,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
return
self._update_duplicate_display(result)
self.mw.query_op(self.note.duplicate_or_empty, success=on_done)
QueryOp(
parent=self.parentWindow,
op=lambda _: self.note.duplicate_or_empty(),
success=on_done,
).run_in_background()
checkValid = _check_and_update_duplicate_display_async

View File

@ -10,6 +10,7 @@ from anki.errors import SearchError
from anki.lang import without_unicode_isolation
from anki.scheduler import FilteredDeckForUpdate
from aqt import AnkiQt, colors, gui_hooks
from aqt.operations import QueryOp
from aqt.operations.scheduling import add_or_update_filtered_deck
from aqt.qt import *
from aqt.theme import theme_manager
@ -56,11 +57,11 @@ class FilteredDeckConfigDialog(QDialog):
# set on successful query
self.deck: FilteredDeckForUpdate
mw.query_op(
lambda: mw.col.sched.get_or_create_filtered_deck(deck_id=deck_id),
QueryOp(
parent=self.mw,
op=lambda col: col.sched.get_or_create_filtered_deck(deck_id=deck_id),
success=self.load_deck_and_show,
failure=self.on_fetch_error,
)
).failure(self.on_fetch_error).run_in_background()
def on_fetch_error(self, exc: Exception) -> None:
showWarning(str(exc))

View File

@ -695,47 +695,6 @@ class AnkiQt(QMainWindow):
# Resetting state
##########################################################################
def query_op(
self,
op: Callable[[], T],
*,
success: Callable[[T], Any] = None,
failure: Optional[Callable[[Exception], Any]] = None,
) -> None:
"""Run an operation that queries the DB on a background thread.
Intended to be used for operations that do not change collection
state. Undo status will not be changed, and `operation_did_execute`
will not fire. No progress window will be shown either.
`operations_will|did_execute` will still fire, so the UI can defer
updates during a background task.
"""
def wrapped_done(future: Future) -> None:
self._decrease_background_ops()
# did something go wrong?
if exception := future.exception():
if isinstance(exception, Exception):
if failure:
failure(exception)
else:
showWarning(str(exception))
return
else:
# BaseException like SystemExit; rethrow it
future.result()
result = future.result()
if success:
success(result)
self._increase_background_ops()
self.taskman.run_in_background(op, wrapped_done)
# Resetting state
##########################################################################
def _increase_background_ops(self) -> None:
if not self._background_op_count:
gui_hooks.backend_will_block()

View File

@ -11,6 +11,7 @@ from anki.lang import without_unicode_isolation
from anki.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount
from anki.notes import Note
from aqt import AnkiQt, gui_hooks
from aqt.operations import QueryOp
from aqt.operations.notetype import (
add_notetype_legacy,
remove_notetype,
@ -107,10 +108,11 @@ class Models(QDialog):
maybeHideClose(box)
def refresh_list(self, *ignored_args: Any) -> None:
self.mw.query_op(
self.col.models.all_use_counts,
QueryOp(
parent=self,
op=lambda col: col.models.all_use_counts(),
success=self.updateModelsList,
)
).run_in_background()
def onRename(self) -> None:
nt = self.current_notetype()

View File

@ -62,7 +62,7 @@ class CollectionOp(Generic[ResultWithChanges]):
"""
_success: Optional[Callable[[ResultWithChanges], Any]] = None
_failure: Optional[Optional[Callable[[Exception], Any]]] = None
_failure: Optional[Callable[[Exception], Any]] = None
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
self._parent = parent
@ -75,7 +75,7 @@ class CollectionOp(Generic[ResultWithChanges]):
return self
def failure(
self, failure: Optional[Optional[Callable[[Exception], Any]]]
self, failure: Optional[Callable[[Exception], Any]]
) -> CollectionOp[ResultWithChanges]:
self._failure = failure
return self
@ -140,3 +140,87 @@ class CollectionOp(Generic[ResultWithChanges]):
# fire legacy hook so old code notices changes
if mw.col.op_made_changes(changes):
aqt.gui_hooks.state_did_reset()
T = TypeVar("T")
class QueryOp(Generic[T]):
"""Helper to perform a non-mutating DB query on a background thread.
- Optionally shows progress popup for the duration of the op.
- Ensures the browser doesn't try to redraw during the operation, which can lead
to a frozen UI
Be careful not to call any UI routines in `op`, as that may crash Qt.
This includes things select .selectedCards() in the browse screen.
`success` will be called with the return value of op().
If op() throws an exception, it will be shown in a popup, or
passed to `failure` if it is provided.
"""
_failure: Optional[Callable[[Exception], Any]] = None
_progress: Union[bool, str] = False
def __init__(
self,
*,
parent: QWidget,
op: Callable[[Collection], T],
success: Callable[[T], Any],
):
self._parent = parent
self._op = op
self._success = success
def failure(self, failure: Optional[Callable[[Exception], Any]]) -> QueryOp[T]:
self._failure = failure
return self
def with_progress(self, label: Optional[str] = None) -> QueryOp[T]:
self._progress = label or True
return self
def run_in_background(self) -> None:
from aqt import mw
assert mw
mw._increase_background_ops()
def wrapped_op() -> T:
assert mw
if self._progress:
label: Optional[str]
if isinstance(self._progress, str):
label = self._progress
else:
label = None
mw.progress.start(label=label)
return self._op(mw.col)
def wrapped_done(future: Future) -> None:
assert mw
mw._decrease_background_ops()
# did something go wrong?
if exception := future.exception():
if isinstance(exception, Exception):
if self._failure:
self._failure(exception)
else:
showWarning(str(exception), self._parent)
return
else:
# BaseException like SystemExit; rethrow it
future.result()
result = future.result()
try:
self._success(result)
finally:
if self._progress:
mw.progress.finish()
mw.taskman.run_in_background(wrapped_op, wrapped_done)