mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00
Remove OpWithBackendProgress and ClosedCollectionOp
Backend progress logic is now in ProgressManager. QueryOp can be used for running on closed collection. Also fix aborting of colpkg exports, which slipped through in #1817.
This commit is contained in:
parent
702f47c522
commit
19664a0e47
6 changed files with 135 additions and 172 deletions
|
@ -7,18 +7,17 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from concurrent.futures import Future
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Sequence, Type
|
from typing import Sequence, Type
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.main
|
import aqt.main
|
||||||
from anki.collection import DeckIdLimit, ExportLimit, NoteIdsLimit
|
from anki.collection import DeckIdLimit, ExportLimit, NoteIdsLimit, Progress
|
||||||
from anki.decks import DeckId, DeckNameId
|
from anki.decks import DeckId, DeckNameId
|
||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.errors import show_exception
|
from aqt.errors import show_exception
|
||||||
from aqt.operations import QueryOpWithBackendProgress
|
from aqt.operations import QueryOp
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
checkInvalidFilename,
|
checkInvalidFilename,
|
||||||
|
@ -180,7 +179,7 @@ class ColpkgExporter(Exporter):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
||||||
def on_success(_future: Future) -> None:
|
def on_success(_: None) -> None:
|
||||||
mw.reopen()
|
mw.reopen()
|
||||||
tooltip(tr.exporting_collection_exported(), parent=mw)
|
tooltip(tr.exporting_collection_exported(), parent=mw)
|
||||||
|
|
||||||
|
@ -189,14 +188,15 @@ class ColpkgExporter(Exporter):
|
||||||
show_exception(parent=mw, exception=exception)
|
show_exception(parent=mw, exception=exception)
|
||||||
|
|
||||||
gui_hooks.collection_will_temporarily_close(mw.col)
|
gui_hooks.collection_will_temporarily_close(mw.col)
|
||||||
QueryOpWithBackendProgress(
|
QueryOp(
|
||||||
parent=mw,
|
parent=mw,
|
||||||
op=lambda col: col.export_collection_package(
|
op=lambda col: col.export_collection_package(
|
||||||
options.out_path, include_media=options.include_media, legacy=False
|
options.out_path, include_media=options.include_media, legacy=False
|
||||||
),
|
),
|
||||||
success=on_success,
|
success=on_success,
|
||||||
key="exporting",
|
).with_backend_progress(export_progress_label).failure(
|
||||||
).failure(on_failure).run_in_background()
|
on_failure
|
||||||
|
).run_in_background()
|
||||||
|
|
||||||
|
|
||||||
class ApkgExporter(Exporter):
|
class ApkgExporter(Exporter):
|
||||||
|
@ -211,7 +211,7 @@ class ApkgExporter(Exporter):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
def export(mw: aqt.main.AnkiQt, options: Options) -> None:
|
||||||
QueryOpWithBackendProgress(
|
QueryOp(
|
||||||
parent=mw,
|
parent=mw,
|
||||||
op=lambda col: col.export_anki_package(
|
op=lambda col: col.export_anki_package(
|
||||||
out_path=options.out_path,
|
out_path=options.out_path,
|
||||||
|
@ -219,8 +219,13 @@ class ApkgExporter(Exporter):
|
||||||
with_scheduling=options.include_scheduling,
|
with_scheduling=options.include_scheduling,
|
||||||
with_media=options.include_media,
|
with_media=options.include_media,
|
||||||
),
|
),
|
||||||
success=lambda fut: tooltip(
|
success=lambda count: tooltip(
|
||||||
tr.exporting_note_exported(count=fut), parent=mw
|
tr.exporting_note_exported(count=count), parent=mw
|
||||||
),
|
),
|
||||||
key="exporting",
|
).with_backend_progress(export_progress_label).run_in_background()
|
||||||
).run_in_background()
|
|
||||||
|
|
||||||
|
def export_progress_label(progress: Progress) -> str | None:
|
||||||
|
if not progress.HasField("exporting"):
|
||||||
|
return None
|
||||||
|
return tr.exporting_exported_media_file(count=progress.exporting)
|
||||||
|
|
|
@ -3,14 +3,12 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from concurrent.futures import Future
|
from typing import Sequence
|
||||||
|
|
||||||
import aqt.main
|
import aqt.main
|
||||||
|
from anki.collection import Collection, ImportLogWithChanges, Progress
|
||||||
from anki.errors import Interrupted
|
from anki.errors import Interrupted
|
||||||
from aqt.operations import (
|
from aqt.operations import CollectionOp, QueryOp
|
||||||
ClosedCollectionOpWithBackendProgress,
|
|
||||||
CollectionOpWithBackendProgress,
|
|
||||||
)
|
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import askUser, getFile, showInfo, showText, showWarning, tooltip, tr
|
from aqt.utils import askUser, getFile, showInfo, showText, showWarning, tooltip, tr
|
||||||
|
|
||||||
|
@ -62,7 +60,7 @@ def maybe_import_collection_package(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
|
def import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
|
||||||
def on_success(_future: Future) -> None:
|
def on_success() -> None:
|
||||||
mw.loadCollection()
|
mw.loadCollection()
|
||||||
tooltip(tr.importing_importing_complete())
|
tooltip(tr.importing_importing_complete())
|
||||||
|
|
||||||
|
@ -71,38 +69,43 @@ def import_collection_package(mw: aqt.main.AnkiQt, file: str) -> None:
|
||||||
if not isinstance(err, Interrupted):
|
if not isinstance(err, Interrupted):
|
||||||
showWarning(str(err))
|
showWarning(str(err))
|
||||||
|
|
||||||
import_collection_package_op(mw, file).success(on_success).failure(
|
QueryOp(
|
||||||
on_failure
|
parent=mw,
|
||||||
).run_in_background()
|
op=lambda _: mw.create_backup_now(),
|
||||||
|
success=lambda _: mw.unloadCollection(
|
||||||
|
lambda: import_collection_package_op(mw, file, on_success)
|
||||||
|
.failure(on_failure)
|
||||||
|
.run_in_background()
|
||||||
|
),
|
||||||
|
).with_progress().run_in_background()
|
||||||
|
|
||||||
|
|
||||||
def import_collection_package_op(
|
def import_collection_package_op(
|
||||||
mw: aqt.main.AnkiQt, path: str
|
mw: aqt.main.AnkiQt, path: str, success: Callable[[], None]
|
||||||
) -> ClosedCollectionOpWithBackendProgress:
|
) -> QueryOp[None]:
|
||||||
def op() -> None:
|
def op(_: Collection) -> None:
|
||||||
col_path = mw.pm.collectionPath()
|
col_path = mw.pm.collectionPath()
|
||||||
media_folder = os.path.join(mw.pm.profileFolder(), "collection.media")
|
media_folder = os.path.join(mw.pm.profileFolder(), "collection.media")
|
||||||
mw.backend.import_collection_package(
|
mw.backend.import_collection_package(
|
||||||
col_path=col_path, backup_path=path, media_folder=media_folder
|
col_path=col_path, backup_path=path, media_folder=media_folder
|
||||||
)
|
)
|
||||||
|
|
||||||
return ClosedCollectionOpWithBackendProgress(
|
return QueryOp(parent=mw, op=op, success=lambda _: success()).with_backend_progress(
|
||||||
parent=mw,
|
import_progress_label
|
||||||
op=op,
|
|
||||||
key="importing",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def import_anki_package(mw: aqt.main.AnkiQt, path: str) -> None:
|
def import_anki_package(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||||
CollectionOpWithBackendProgress(
|
CollectionOp(
|
||||||
parent=mw,
|
parent=mw,
|
||||||
op=lambda col: col.import_anki_package(path),
|
op=lambda col: col.import_anki_package(path),
|
||||||
key="importing",
|
).with_backend_progress(import_progress_label).success(
|
||||||
).success(show_import_log).run_in_background()
|
show_import_log
|
||||||
|
).run_in_background()
|
||||||
|
|
||||||
|
|
||||||
def show_import_log(future: Future) -> None:
|
def show_import_log(log_with_changes: ImportLogWithChanges) -> None:
|
||||||
log = future.log # type: ignore
|
log = log_with_changes.log # type: ignore
|
||||||
total = len(log.conflicting) + len(log.updated) + len(log.new) + len(log.duplicate)
|
total = len(log.conflicting) + len(log.updated) + len(log.new) + len(log.duplicate)
|
||||||
|
|
||||||
text = f"""{tr.importing_notes_found_in_file(val=total)}
|
text = f"""{tr.importing_notes_found_in_file(val=total)}
|
||||||
|
@ -120,5 +123,9 @@ def show_import_log(future: Future) -> None:
|
||||||
showText(text, plain_text_edit=True)
|
showText(text, plain_text_edit=True)
|
||||||
|
|
||||||
|
|
||||||
def log_rows(rows: list, action: str) -> str:
|
def log_rows(rows: Sequence, action: str) -> str:
|
||||||
return "\n".join(f"[{action}] {', '.join(note.fields)}" for note in rows)
|
return "\n".join(f"[{action}] {', '.join(note.fields)}" for note in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def import_progress_label(progress: Progress) -> str | None:
|
||||||
|
return progress.importing if progress.HasField("importing") else None
|
||||||
|
|
|
@ -407,8 +407,8 @@ class AnkiQt(QMainWindow):
|
||||||
self.restoring_backup = True
|
self.restoring_backup = True
|
||||||
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
|
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
|
||||||
|
|
||||||
import_collection_package_op(self, path).success(
|
import_collection_package_op(
|
||||||
lambda _: self.onOpenProfile()
|
self, path, success=self.onOpenProfile
|
||||||
).run_in_background()
|
).run_in_background()
|
||||||
|
|
||||||
def _on_downgrade(self) -> None:
|
def _on_downgrade(self) -> None:
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC
|
|
||||||
from concurrent.futures._base import Future
|
from concurrent.futures._base import Future
|
||||||
from typing import Any, Callable, Generic, Literal, Protocol, TypeVar, Union
|
from typing import Any, Callable, Generic, Protocol, TypeVar, Union
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.gui_hooks
|
import aqt.gui_hooks
|
||||||
|
@ -20,8 +19,7 @@ from anki.collection import (
|
||||||
Progress,
|
Progress,
|
||||||
)
|
)
|
||||||
from aqt.errors import show_exception
|
from aqt.errors import show_exception
|
||||||
from aqt.qt import QTimer, QWidget, qconnect
|
from aqt.qt import QWidget
|
||||||
from aqt.utils import tr
|
|
||||||
|
|
||||||
|
|
||||||
class HasChangesProperty(Protocol):
|
class HasChangesProperty(Protocol):
|
||||||
|
@ -70,6 +68,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
||||||
|
|
||||||
_success: Callable[[ResultWithChanges], Any] | None = None
|
_success: Callable[[ResultWithChanges], Any] | None = None
|
||||||
_failure: Callable[[Exception], Any] | None = None
|
_failure: Callable[[Exception], Any] | None = None
|
||||||
|
_label_from_progress: Callable[[Progress], str | None] | None = 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
|
||||||
|
@ -87,6 +86,12 @@ class CollectionOp(Generic[ResultWithChanges]):
|
||||||
self._failure = failure
|
self._failure = failure
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def with_backend_progress(
|
||||||
|
self, label_from_progress: Callable[[Progress], str | None] | None
|
||||||
|
) -> CollectionOp[ResultWithChanges]:
|
||||||
|
self._label_from_progress = label_from_progress
|
||||||
|
return self
|
||||||
|
|
||||||
def run_in_background(self, *, initiator: object | None = None) -> None:
|
def run_in_background(self, *, initiator: object | None = None) -> None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
|
@ -128,7 +133,12 @@ class CollectionOp(Generic[ResultWithChanges]):
|
||||||
op: Callable[[], ResultWithChanges],
|
op: Callable[[], ResultWithChanges],
|
||||||
on_done: Callable[[Future], None],
|
on_done: Callable[[Future], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
mw.taskman.with_progress(op, on_done)
|
if self._label_from_progress:
|
||||||
|
mw.taskman.with_backend_progress(
|
||||||
|
op, self._label_from_progress, on_done=on_done, parent=self._parent
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mw.taskman.with_progress(op, on_done, parent=self._parent)
|
||||||
|
|
||||||
def _finish_op(
|
def _finish_op(
|
||||||
self, mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None
|
self, mw: aqt.main.AnkiQt, result: ResultWithChanges, initiator: object | None
|
||||||
|
@ -185,6 +195,7 @@ class QueryOp(Generic[T]):
|
||||||
|
|
||||||
_failure: Callable[[Exception], Any] | None = None
|
_failure: Callable[[Exception], Any] | None = None
|
||||||
_progress: bool | str = False
|
_progress: bool | str = False
|
||||||
|
_label_from_progress: Callable[[Progress], str | None] | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -209,6 +220,12 @@ class QueryOp(Generic[T]):
|
||||||
self._progress = label or True
|
self._progress = label or True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def with_backend_progress(
|
||||||
|
self, label_from_progress: Callable[[Progress], str | None] | None
|
||||||
|
) -> QueryOp[T]:
|
||||||
|
self._label_from_progress = label_from_progress
|
||||||
|
return self
|
||||||
|
|
||||||
def run_in_background(self) -> None:
|
def run_in_background(self) -> None:
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
|
||||||
|
@ -218,26 +235,11 @@ class QueryOp(Generic[T]):
|
||||||
|
|
||||||
def wrapped_op() -> T:
|
def wrapped_op() -> T:
|
||||||
assert mw
|
assert mw
|
||||||
if self._progress:
|
|
||||||
label: str | None
|
|
||||||
if isinstance(self._progress, str):
|
|
||||||
label = self._progress
|
|
||||||
else:
|
|
||||||
label = None
|
|
||||||
|
|
||||||
def start_progress() -> None:
|
|
||||||
assert mw
|
|
||||||
mw.progress.start(label=label)
|
|
||||||
|
|
||||||
mw.taskman.run_on_main(start_progress)
|
|
||||||
return self._op(mw.col)
|
return self._op(mw.col)
|
||||||
|
|
||||||
def wrapped_done(future: Future) -> None:
|
def wrapped_done(future: Future) -> None:
|
||||||
assert mw
|
assert mw
|
||||||
|
|
||||||
if self._progress:
|
|
||||||
mw.progress.finish()
|
|
||||||
|
|
||||||
mw._decrease_background_ops()
|
mw._decrease_background_ops()
|
||||||
# did something go wrong?
|
# did something go wrong?
|
||||||
if exception := future.exception():
|
if exception := future.exception():
|
||||||
|
@ -261,120 +263,16 @@ class QueryOp(Generic[T]):
|
||||||
op: Callable[[], T],
|
op: Callable[[], T],
|
||||||
on_done: Callable[[Future], None],
|
on_done: Callable[[Future], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
mw.taskman.run_in_background(op, on_done)
|
label = self._progress if isinstance(self._progress, str) else None
|
||||||
|
if self._label_from_progress:
|
||||||
|
mw.taskman.with_backend_progress(
|
||||||
class OpWithBackendProgress(ABC):
|
op,
|
||||||
"""Periodically queries the backend for progress updates, and enables abortion.
|
self._label_from_progress,
|
||||||
|
on_done=on_done,
|
||||||
Requires a key for a value on the `Progress` proto message."""
|
start_label=label,
|
||||||
|
parent=self._parent,
|
||||||
def __init__(
|
)
|
||||||
self,
|
elif self._progress:
|
||||||
*args: Any,
|
mw.taskman.with_progress(op, on_done, label=label, parent=self._parent)
|
||||||
key: Literal["importing", "exporting"],
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
self._key = key
|
|
||||||
self._timer = QTimer()
|
|
||||||
self._timer.setSingleShot(False)
|
|
||||||
self._timer.setInterval(100)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def _run(
|
|
||||||
self,
|
|
||||||
mw: aqt.main.AnkiQt,
|
|
||||||
op: Callable,
|
|
||||||
on_done: Callable[[Future], None],
|
|
||||||
) -> None:
|
|
||||||
if not (dialog := mw.progress.start(immediate=True, parent=mw)):
|
|
||||||
print("Progress dialog already running; aborting will not work")
|
|
||||||
|
|
||||||
def on_progress() -> None:
|
|
||||||
assert mw
|
|
||||||
|
|
||||||
progress = mw.backend.latest_progress()
|
|
||||||
if not (label := label_from_progress(progress, self._key)):
|
|
||||||
return
|
|
||||||
if dialog and dialog.wantCancel:
|
|
||||||
mw.backend.set_wants_abort()
|
|
||||||
mw.taskman.run_on_main(lambda: mw.progress.update(label=label))
|
|
||||||
|
|
||||||
def wrapped_on_done(future: Future) -> None:
|
|
||||||
self._timer.deleteLater()
|
|
||||||
assert mw
|
|
||||||
mw.progress.finish()
|
|
||||||
on_done(future)
|
|
||||||
|
|
||||||
qconnect(self._timer.timeout, on_progress)
|
|
||||||
self._timer.start()
|
|
||||||
mw.taskman.run_in_background(task=op, on_done=wrapped_on_done)
|
|
||||||
|
|
||||||
|
|
||||||
def label_from_progress(
|
|
||||||
progress: Progress,
|
|
||||||
key: Literal["importing", "exporting"],
|
|
||||||
) -> str | None:
|
|
||||||
if not progress.HasField(key):
|
|
||||||
return None
|
|
||||||
if key == "importing":
|
|
||||||
return progress.importing
|
|
||||||
if key == "exporting":
|
|
||||||
return tr.exporting_exported_media_file(count=progress.exporting)
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionOpWithBackendProgress(OpWithBackendProgress, CollectionOp):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class QueryOpWithBackendProgress(OpWithBackendProgress, QueryOp):
|
|
||||||
def with_progress(self, *_args: Any) -> Any:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ClosedCollectionOp(CollectionOp):
|
|
||||||
"""For CollectionOps that need to be run on a closed collection.
|
|
||||||
|
|
||||||
If a collection is open, backs it up and unloads it, before running the op.
|
|
||||||
Reloads it, if that has not been done by a callback, yet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
parent: QWidget,
|
|
||||||
op: Callable[[], ResultWithChanges],
|
|
||||||
*args: Any,
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
super().__init__(parent, lambda _: op(), *args, **kwargs)
|
|
||||||
|
|
||||||
def _run(
|
|
||||||
self,
|
|
||||||
mw: aqt.main.AnkiQt,
|
|
||||||
op: Callable[[], ResultWithChanges],
|
|
||||||
on_done: Callable[[Future], None],
|
|
||||||
) -> None:
|
|
||||||
if mw.col:
|
|
||||||
QueryOp(
|
|
||||||
parent=mw,
|
|
||||||
op=lambda _: mw.create_backup_now(),
|
|
||||||
success=lambda _: mw.unloadCollection(
|
|
||||||
lambda: super(ClosedCollectionOp, self)._run(mw, op, on_done)
|
|
||||||
),
|
|
||||||
).with_progress().run_in_background()
|
|
||||||
else:
|
else:
|
||||||
super()._run(mw, op, on_done)
|
mw.taskman.run_in_background(op, on_done)
|
||||||
|
|
||||||
def _finish_op(
|
|
||||||
self,
|
|
||||||
_mw: aqt.main.AnkiQt,
|
|
||||||
_result: ResultWithChanges,
|
|
||||||
_initiator: object | None,
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ClosedCollectionOpWithBackendProgress(
|
|
||||||
ClosedCollectionOp, CollectionOpWithBackendProgress
|
|
||||||
):
|
|
||||||
"""See ClosedCollectionOp and CollectionOpWithBackendProgress."""
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import time
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki._legacy import print_deprecation_warning
|
from anki._legacy import print_deprecation_warning
|
||||||
|
from anki.collection import Progress
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import disable_help_button, tr
|
from aqt.utils import disable_help_button, tr
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ class ProgressManager:
|
||||||
self._busy_cursor_timer: QTimer | None = None
|
self._busy_cursor_timer: QTimer | None = None
|
||||||
self._win: ProgressDialog | None = None
|
self._win: ProgressDialog | None = None
|
||||||
self._levels = 0
|
self._levels = 0
|
||||||
|
self._backend_timer: QTimer | None = None
|
||||||
|
|
||||||
# Safer timers
|
# Safer timers
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -166,6 +168,32 @@ class ProgressManager:
|
||||||
qconnect(self._show_timer.timeout, self._on_show_timer)
|
qconnect(self._show_timer.timeout, self._on_show_timer)
|
||||||
return self._win
|
return self._win
|
||||||
|
|
||||||
|
def start_with_backend_updates(
|
||||||
|
self,
|
||||||
|
label_from_progress: Callable[[Progress], str | None],
|
||||||
|
start_label: str | None = None,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
) -> None:
|
||||||
|
self._backend_timer = QTimer()
|
||||||
|
self._backend_timer.setSingleShot(False)
|
||||||
|
self._backend_timer.setInterval(100)
|
||||||
|
|
||||||
|
if not (dialog := self.start(immediate=True, label=start_label, parent=parent)):
|
||||||
|
print("Progress dialog already running; aborting will not work")
|
||||||
|
|
||||||
|
def on_progress() -> None:
|
||||||
|
assert self.mw
|
||||||
|
|
||||||
|
progress = self.mw.backend.latest_progress()
|
||||||
|
if not (label := label_from_progress(progress)):
|
||||||
|
return
|
||||||
|
if dialog and dialog.wantCancel:
|
||||||
|
self.mw.backend.set_wants_abort()
|
||||||
|
self.update(label=label)
|
||||||
|
|
||||||
|
qconnect(self._backend_timer.timeout, on_progress)
|
||||||
|
self._backend_timer.start()
|
||||||
|
|
||||||
def update(
|
def update(
|
||||||
self,
|
self,
|
||||||
label: str | None = None,
|
label: str | None = None,
|
||||||
|
@ -204,6 +232,9 @@ class ProgressManager:
|
||||||
if self._show_timer:
|
if self._show_timer:
|
||||||
self._show_timer.stop()
|
self._show_timer.stop()
|
||||||
self._show_timer = None
|
self._show_timer = None
|
||||||
|
if self._backend_timer:
|
||||||
|
self._backend_timer.deleteLater()
|
||||||
|
self._backend_timer = None
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"Restore the interface after an error."
|
"Restore the interface after an error."
|
||||||
|
|
|
@ -15,6 +15,7 @@ from threading import Lock
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki.collection import Progress
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
|
||||||
Closure = Callable[[], None]
|
Closure = Callable[[], None]
|
||||||
|
@ -89,6 +90,27 @@ class TaskManager(QObject):
|
||||||
|
|
||||||
self.run_in_background(task, wrapped_done)
|
self.run_in_background(task, wrapped_done)
|
||||||
|
|
||||||
|
def with_backend_progress(
|
||||||
|
self,
|
||||||
|
task: Callable,
|
||||||
|
label_from_progress: Callable[[Progress], str | None],
|
||||||
|
on_done: Callable[[Future], None] | None = None,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
start_label: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.mw.progress.start_with_backend_updates(
|
||||||
|
label_from_progress,
|
||||||
|
parent=parent,
|
||||||
|
start_label=start_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
def _on_closures_pending(self) -> None:
|
||||||
"""Run any pending closures. This runs in the main thread."""
|
"""Run any pending closures. This runs in the main thread."""
|
||||||
with self._closures_lock:
|
with self._closures_lock:
|
||||||
|
|
Loading…
Reference in a new issue