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
|
import aqt
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from aqt import AnkiQt
|
from aqt import AnkiQt
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.note import find_and_replace
|
from aqt.operations.note import find_and_replace
|
||||||
from aqt.operations.tag import find_and_replace_tag
|
from aqt.operations.tag import find_and_replace_tag
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
@ -40,10 +41,11 @@ class FindAndReplaceDialog(QDialog):
|
|||||||
self.field_names: List[str] = []
|
self.field_names: List[str] = []
|
||||||
|
|
||||||
# fetch field names and then show
|
# fetch field names and then show
|
||||||
mw.query_op(
|
QueryOp(
|
||||||
lambda: mw.col.field_names_for_note_ids(note_ids),
|
parent=mw,
|
||||||
|
op=lambda col: col.field_names_for_note_ids(note_ids),
|
||||||
success=self._show,
|
success=self._show,
|
||||||
)
|
).run_in_background()
|
||||||
|
|
||||||
def _show(self, field_names: Sequence[str]) -> None:
|
def _show(self, field_names: Sequence[str]) -> None:
|
||||||
# add "all fields" and "tags" to the top of the list
|
# 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.browser.sidebar.toolbar import SidebarTool, SidebarToolbar
|
||||||
from aqt.clayout import CardLayout
|
from aqt.clayout import CardLayout
|
||||||
from aqt.models import Models
|
from aqt.models import Models
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.deck import (
|
from aqt.operations.deck import (
|
||||||
remove_decks,
|
remove_decks,
|
||||||
rename_deck,
|
rename_deck,
|
||||||
@ -163,7 +164,9 @@ class SidebarTreeView(QTreeView):
|
|||||||
# needs to be set after changing model
|
# needs to be set after changing model
|
||||||
qconnect(self.selectionModel().selectionChanged, self._on_selection_changed)
|
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:
|
def restore_current(self, current: SidebarItem) -> None:
|
||||||
if current := self.find_item(current.has_same_id):
|
if current := self.find_item(current.has_same_id):
|
||||||
@ -892,7 +895,11 @@ class SidebarTreeView(QTreeView):
|
|||||||
new_name=full_name,
|
new_name=full_name,
|
||||||
).run_in_background()
|
).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:
|
def delete_decks(self, _item: SidebarItem) -> None:
|
||||||
remove_decks(parent=self, deck_ids=self._selected_decks()).run_in_background()
|
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.decks import Deck, DeckCollapseScope, DeckId, DeckTreeNode
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.deck import (
|
from aqt.operations.deck import (
|
||||||
add_deck_dialog,
|
add_deck_dialog,
|
||||||
remove_decks,
|
remove_decks,
|
||||||
@ -284,7 +285,9 @@ class DeckBrowser:
|
|||||||
parent=self.mw, deck_id=did, new_name=new_name
|
parent=self.mw, deck_id=did, new_name=new_name
|
||||||
).run_in_background()
|
).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:
|
def _options(self, did: DeckId) -> None:
|
||||||
# select the deck first, because the dyn deck conf assumes the deck
|
# select the deck first, because the dyn deck conf assumes the deck
|
||||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.decks import Deck
|
from anki.decks import Deck
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.deck import update_deck
|
from aqt.operations.deck import update_deck
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
|
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
|
||||||
@ -22,7 +23,11 @@ class DeckDescriptionDialog(QDialog):
|
|||||||
# set on success
|
# set on success
|
||||||
self.deck: Deck
|
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:
|
def _setup_and_show(self, deck: Deck) -> None:
|
||||||
if deck.WhichOneof("kind") != "normal":
|
if deck.WhichOneof("kind") != "normal":
|
||||||
|
@ -30,6 +30,7 @@ from anki.httpclient import HttpClient
|
|||||||
from anki.notes import DuplicateOrEmptyResult, Note
|
from anki.notes import DuplicateOrEmptyResult, Note
|
||||||
from anki.utils import checksum, isLin, isWin, namedtmp
|
from anki.utils import checksum, isLin, isWin, namedtmp
|
||||||
from aqt import AnkiQt, colors, gui_hooks
|
from aqt import AnkiQt, colors, gui_hooks
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.note import update_note
|
from aqt.operations.note import update_note
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.sound import av_player
|
from aqt.sound import av_player
|
||||||
@ -496,7 +497,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
|||||||
return
|
return
|
||||||
self._update_duplicate_display(result)
|
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
|
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.lang import without_unicode_isolation
|
||||||
from anki.scheduler import FilteredDeckForUpdate
|
from anki.scheduler import FilteredDeckForUpdate
|
||||||
from aqt import AnkiQt, colors, gui_hooks
|
from aqt import AnkiQt, colors, gui_hooks
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.scheduling import add_or_update_filtered_deck
|
from aqt.operations.scheduling import add_or_update_filtered_deck
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
@ -56,11 +57,11 @@ class FilteredDeckConfigDialog(QDialog):
|
|||||||
# set on successful query
|
# set on successful query
|
||||||
self.deck: FilteredDeckForUpdate
|
self.deck: FilteredDeckForUpdate
|
||||||
|
|
||||||
mw.query_op(
|
QueryOp(
|
||||||
lambda: mw.col.sched.get_or_create_filtered_deck(deck_id=deck_id),
|
parent=self.mw,
|
||||||
|
op=lambda col: col.sched.get_or_create_filtered_deck(deck_id=deck_id),
|
||||||
success=self.load_deck_and_show,
|
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:
|
def on_fetch_error(self, exc: Exception) -> None:
|
||||||
showWarning(str(exc))
|
showWarning(str(exc))
|
||||||
|
@ -695,47 +695,6 @@ class AnkiQt(QMainWindow):
|
|||||||
# Resetting state
|
# 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:
|
def _increase_background_ops(self) -> None:
|
||||||
if not self._background_op_count:
|
if not self._background_op_count:
|
||||||
gui_hooks.backend_will_block()
|
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.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
from aqt.operations import QueryOp
|
||||||
from aqt.operations.notetype import (
|
from aqt.operations.notetype import (
|
||||||
add_notetype_legacy,
|
add_notetype_legacy,
|
||||||
remove_notetype,
|
remove_notetype,
|
||||||
@ -107,10 +108,11 @@ class Models(QDialog):
|
|||||||
maybeHideClose(box)
|
maybeHideClose(box)
|
||||||
|
|
||||||
def refresh_list(self, *ignored_args: Any) -> None:
|
def refresh_list(self, *ignored_args: Any) -> None:
|
||||||
self.mw.query_op(
|
QueryOp(
|
||||||
self.col.models.all_use_counts,
|
parent=self,
|
||||||
|
op=lambda col: col.models.all_use_counts(),
|
||||||
success=self.updateModelsList,
|
success=self.updateModelsList,
|
||||||
)
|
).run_in_background()
|
||||||
|
|
||||||
def onRename(self) -> None:
|
def onRename(self) -> None:
|
||||||
nt = self.current_notetype()
|
nt = self.current_notetype()
|
||||||
|
@ -62,7 +62,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_success: Optional[Callable[[ResultWithChanges], Any]] = None
|
_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]):
|
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
@ -75,7 +75,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def failure(
|
def failure(
|
||||||
self, failure: Optional[Optional[Callable[[Exception], Any]]]
|
self, failure: Optional[Callable[[Exception], Any]]
|
||||||
) -> CollectionOp[ResultWithChanges]:
|
) -> CollectionOp[ResultWithChanges]:
|
||||||
self._failure = failure
|
self._failure = failure
|
||||||
return self
|
return self
|
||||||
@ -140,3 +140,87 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||||||
# fire legacy hook so old code notices changes
|
# fire legacy hook so old code notices changes
|
||||||
if mw.col.op_made_changes(changes):
|
if mw.col.op_made_changes(changes):
|
||||||
aqt.gui_hooks.state_did_reset()
|
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