move query_op into operations/, and add the ability to show progress
This commit is contained in:
parent
b887032244
commit
f2db822c08
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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":
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user