mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
update find&replace, and remove perform_op()
This commit is contained in:
parent
84fe309583
commit
5676ad5101
11 changed files with 108 additions and 191 deletions
|
@ -3,11 +3,13 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence
|
||||
from typing import List, Sequence
|
||||
|
||||
import aqt
|
||||
from anki.notes import NoteId
|
||||
from aqt import AnkiQt, QWidget
|
||||
from aqt.operations.note import find_and_replace
|
||||
from aqt.operations.tag import find_and_replace_tag
|
||||
from aqt.qt import QDialog, Qt
|
||||
from aqt.utils import (
|
||||
HelpPage,
|
||||
|
@ -22,63 +24,10 @@ from aqt.utils import (
|
|||
save_combo_index_for_session,
|
||||
save_is_checked,
|
||||
saveGeom,
|
||||
tooltip,
|
||||
tr,
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
field_name: Optional[str],
|
||||
match_case: bool,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
field_name=field_name,
|
||||
match_case=match_case,
|
||||
),
|
||||
success=lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace_tag(
|
||||
*,
|
||||
mw: AnkiQt,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[int],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
match_case: bool,
|
||||
) -> None:
|
||||
mw.perform_op(
|
||||
lambda: mw.col.tags.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
match_case=match_case,
|
||||
),
|
||||
success=lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class FindAndReplaceDialog(QDialog):
|
||||
COMBO_NAME = "BrowserFindAndReplace"
|
||||
|
||||
|
@ -146,10 +95,9 @@ class FindAndReplaceDialog(QDialog):
|
|||
save_is_checked(self.form.re, self.COMBO_NAME + "Regex")
|
||||
save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase")
|
||||
|
||||
# tags?
|
||||
if self.form.field.currentIndex() == 1:
|
||||
# tags
|
||||
find_and_replace_tag(
|
||||
mw=self.mw,
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
|
@ -157,23 +105,22 @@ class FindAndReplaceDialog(QDialog):
|
|||
regex=regex,
|
||||
match_case=match_case,
|
||||
)
|
||||
return
|
||||
|
||||
if self.form.field.currentIndex() == 0:
|
||||
field = None
|
||||
else:
|
||||
field = self.field_names[self.form.field.currentIndex() - 2]
|
||||
# fields
|
||||
if self.form.field.currentIndex() == 0:
|
||||
field = None
|
||||
else:
|
||||
field = self.field_names[self.form.field.currentIndex() - 2]
|
||||
|
||||
find_and_replace(
|
||||
mw=self.mw,
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
replacement=replace,
|
||||
regex=regex,
|
||||
field_name=field,
|
||||
match_case=match_case,
|
||||
)
|
||||
find_and_replace(
|
||||
parent=self.parentWidget(),
|
||||
note_ids=self.note_ids,
|
||||
search=search,
|
||||
replacement=replace,
|
||||
regex=regex,
|
||||
field_name=field,
|
||||
match_case=match_case,
|
||||
)
|
||||
|
||||
super().accept()
|
||||
|
||||
|
|
|
@ -52,11 +52,6 @@ from aqt.emptycards import show_empty_cards
|
|||
from aqt.legacy import install_pylib_legacy
|
||||
from aqt.mediacheck import check_media_db
|
||||
from aqt.mediasync import MediaSyncer
|
||||
from aqt.operations import (
|
||||
CollectionOpFailureCallback,
|
||||
CollectionOpSuccessCallback,
|
||||
ResultWithChanges,
|
||||
)
|
||||
from aqt.operations.collection import undo
|
||||
from aqt.profiles import ProfileManager as ProfileManagerType
|
||||
from aqt.qt import *
|
||||
|
@ -96,9 +91,6 @@ MainWindowState = Literal[
|
|||
|
||||
T = TypeVar("T")
|
||||
|
||||
PerformOpOptionalSuccessCallback = Optional[CollectionOpSuccessCallback]
|
||||
PerformOpOptionalFailureCallback = Optional[CollectionOpFailureCallback]
|
||||
|
||||
|
||||
class AnkiQt(QMainWindow):
|
||||
col: Collection
|
||||
|
@ -710,10 +702,9 @@ class AnkiQt(QMainWindow):
|
|||
) -> None:
|
||||
"""Run an operation that queries the DB on a background thread.
|
||||
|
||||
Similar interface to perform_op(), but 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.
|
||||
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.
|
||||
|
@ -743,66 +734,6 @@ class AnkiQt(QMainWindow):
|
|||
# Resetting state
|
||||
##########################################################################
|
||||
|
||||
def perform_op(
|
||||
self,
|
||||
op: Callable[[], ResultWithChanges],
|
||||
*,
|
||||
success: PerformOpOptionalSuccessCallback = None,
|
||||
failure: PerformOpOptionalFailureCallback = None,
|
||||
handler: Optional[object] = None,
|
||||
) -> None:
|
||||
"""Run the provided operation on a background thread.
|
||||
|
||||
op() should either return OpChanges, or an object with a 'changes'
|
||||
property. The changes will be passed to `operation_did_execute` so that
|
||||
the UI can decide whether it needs to update itself.
|
||||
|
||||
- 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
|
||||
- Updates undo state at the end of the operation
|
||||
- Commits changes
|
||||
- Fires the `operation_(will|did)_reset` hooks
|
||||
- Fires the legacy `state_did_reset` hook
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
self._increase_background_ops()
|
||||
|
||||
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()
|
||||
try:
|
||||
if success:
|
||||
success(result)
|
||||
finally:
|
||||
# update undo status
|
||||
status = self.col.undo_status()
|
||||
self._update_undo_actions_for_status_and_save(status)
|
||||
# fire change hooks
|
||||
self._fire_change_hooks_after_op_performed(result, handler)
|
||||
|
||||
self.taskman.with_progress(op, wrapped_done)
|
||||
|
||||
def _increase_background_ops(self) -> None:
|
||||
if not self._background_op_count:
|
||||
gui_hooks.backend_will_block()
|
||||
|
@ -814,24 +745,6 @@ class AnkiQt(QMainWindow):
|
|||
gui_hooks.backend_did_block()
|
||||
assert self._background_op_count >= 0
|
||||
|
||||
def _fire_change_hooks_after_op_performed(
|
||||
self,
|
||||
result: ResultWithChanges,
|
||||
handler: Optional[object],
|
||||
) -> None:
|
||||
if isinstance(result, OpChanges):
|
||||
changes = result
|
||||
else:
|
||||
changes = result.changes
|
||||
|
||||
# fire new hook
|
||||
print("op changes:")
|
||||
print(changes)
|
||||
gui_hooks.operation_did_execute(changes, handler)
|
||||
# fire legacy hook so old code notices changes
|
||||
if self.col.op_made_changes(changes):
|
||||
gui_hooks.state_did_reset()
|
||||
|
||||
def _synthesize_op_did_execute_from_reset(self) -> None:
|
||||
"""Fire the `operation_did_execute` hook with everything marked as changed,
|
||||
after legacy code has called .reset()"""
|
||||
|
@ -879,7 +792,7 @@ class AnkiQt(QMainWindow):
|
|||
def reset(self, unused_arg: bool = False) -> None:
|
||||
"""Legacy method of telling UI to refresh after changes made to DB.
|
||||
|
||||
New code should use mw.perform_op() instead."""
|
||||
New code should use CollectionOp() instead."""
|
||||
if self.col:
|
||||
# fire new `operation_did_execute` hook first. If the overview
|
||||
# or review screen are currently open, they will rebuild the study
|
||||
|
|
|
@ -36,9 +36,6 @@ ResultWithChanges = TypeVar(
|
|||
],
|
||||
)
|
||||
|
||||
CollectionOpSuccessCallback = Callable[[ResultWithChanges], Any]
|
||||
CollectionOpFailureCallback = Optional[Callable[[Exception], Any]]
|
||||
|
||||
|
||||
class CollectionOp(Generic[ResultWithChanges]):
|
||||
"""Helper to perform a mutating DB operation on a background thread, and update UI.
|
||||
|
@ -65,7 +62,7 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
"""
|
||||
|
||||
_success: Optional[Callable[[ResultWithChanges], Any]] = None
|
||||
_failure: Optional[CollectionOpFailureCallback] = None
|
||||
_failure: Optional[Optional[Callable[[Exception], Any]]] = None
|
||||
|
||||
def __init__(self, parent: QWidget, op: Callable[[Collection], ResultWithChanges]):
|
||||
self._parent = parent
|
||||
|
@ -78,19 +75,25 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
return self
|
||||
|
||||
def failure(
|
||||
self, failure: Optional[CollectionOpFailureCallback]
|
||||
self, failure: Optional[Optional[Callable[[Exception], Any]]]
|
||||
) -> CollectionOp[ResultWithChanges]:
|
||||
self._failure = failure
|
||||
return self
|
||||
|
||||
def run_in_background(self, *, initiator: Optional[object] = None) -> None:
|
||||
aqt.mw._increase_background_ops()
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
|
||||
mw._increase_background_ops()
|
||||
|
||||
def wrapped_op() -> ResultWithChanges:
|
||||
return self._op(aqt.mw.col)
|
||||
assert mw
|
||||
return self._op(mw.col)
|
||||
|
||||
def wrapped_done(future: Future) -> None:
|
||||
aqt.mw._decrease_background_ops()
|
||||
assert mw
|
||||
mw._decrease_background_ops()
|
||||
# did something go wrong?
|
||||
if exception := future.exception():
|
||||
if isinstance(exception, Exception):
|
||||
|
@ -109,18 +112,22 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
self._success(result)
|
||||
finally:
|
||||
# update undo status
|
||||
status = aqt.mw.col.undo_status()
|
||||
aqt.mw._update_undo_actions_for_status_and_save(status)
|
||||
status = mw.col.undo_status()
|
||||
mw._update_undo_actions_for_status_and_save(status)
|
||||
# fire change hooks
|
||||
self._fire_change_hooks_after_op_performed(result, initiator)
|
||||
|
||||
aqt.mw.taskman.with_progress(wrapped_op, wrapped_done)
|
||||
mw.taskman.with_progress(wrapped_op, wrapped_done)
|
||||
|
||||
def _fire_change_hooks_after_op_performed(
|
||||
self,
|
||||
result: ResultWithChanges,
|
||||
handler: Optional[object],
|
||||
) -> None:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
|
||||
if isinstance(result, OpChanges):
|
||||
changes = result
|
||||
else:
|
||||
|
@ -131,5 +138,5 @@ class CollectionOp(Generic[ResultWithChanges]):
|
|||
print(changes)
|
||||
aqt.gui_hooks.operation_did_execute(changes, handler)
|
||||
# fire legacy hook so old code notices changes
|
||||
if aqt.mw.col.op_made_changes(changes):
|
||||
if mw.col.op_made_changes(changes):
|
||||
aqt.gui_hooks.state_did_reset()
|
||||
|
|
|
@ -31,6 +31,9 @@ def undo(*, parent: QWidget) -> None:
|
|||
def _legacy_undo(*, parent: QWidget) -> None:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
assert mw.col
|
||||
|
||||
reviewing = mw.state == "review"
|
||||
just_refresh_reviewer = False
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from anki.collection import OpChanges, OpChangesWithCount
|
||||
from anki.decks import DeckId
|
||||
|
@ -34,3 +34,31 @@ def remove_notes(
|
|||
return CollectionOp(parent, lambda col: col.remove_notes(note_ids)).success(
|
||||
lambda out: tooltip(tr.browsing_cards_deleted(count=out.count)),
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace(
|
||||
*,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[NoteId],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
field_name: Optional[str],
|
||||
match_case: bool,
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
field_name=field_name,
|
||||
match_case=match_case,
|
||||
),
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -28,6 +28,7 @@ def set_due_date_dialog(
|
|||
card_ids: Sequence[CardId],
|
||||
config_key: Optional[Config.String.Key.V],
|
||||
) -> Optional[CollectionOp[OpChanges]]:
|
||||
assert aqt.mw
|
||||
if not card_ids:
|
||||
return None
|
||||
|
||||
|
@ -76,7 +77,9 @@ def reposition_new_cards_dialog(
|
|||
) -> Optional[CollectionOp[OpChangesWithCount]]:
|
||||
from aqt import mw
|
||||
|
||||
assert mw
|
||||
assert mw.col.db
|
||||
|
||||
row = mw.col.db.first(
|
||||
f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0"
|
||||
)
|
||||
|
|
|
@ -90,3 +90,29 @@ def set_tag_collapsed(
|
|||
return CollectionOp(
|
||||
parent, lambda col: col.tags.set_collapsed(tag=tag, collapsed=collapsed)
|
||||
)
|
||||
|
||||
|
||||
def find_and_replace_tag(
|
||||
*,
|
||||
parent: QWidget,
|
||||
note_ids: Sequence[int],
|
||||
search: str,
|
||||
replacement: str,
|
||||
regex: bool,
|
||||
match_case: bool,
|
||||
) -> CollectionOp[OpChangesWithCount]:
|
||||
return CollectionOp(
|
||||
parent,
|
||||
lambda col: col.tags.find_and_replace(
|
||||
note_ids=note_ids,
|
||||
search=search,
|
||||
replacement=replacement,
|
||||
regex=regex,
|
||||
match_case=match_case,
|
||||
),
|
||||
).success(
|
||||
lambda out: tooltip(
|
||||
tr.findreplace_notes_updated(changed=out.count, total=len(note_ids)),
|
||||
parent=parent,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"""
|
||||
Helper for running tasks on background threads.
|
||||
|
||||
See mw.query_op() and mw.perform_op() for slightly higher-level routines.
|
||||
See mw.query_op() and CollectionOp() for higher-level routines.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
12
qt/mypy.ini
12
qt/mypy.ini
|
@ -9,17 +9,7 @@ check_untyped_defs = true
|
|||
disallow_untyped_defs = True
|
||||
strict_equality = true
|
||||
|
||||
[mypy-aqt.scheduling_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.note_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.card_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.deck_ops]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.find_and_replace]
|
||||
no_strict_optional = false
|
||||
[mypy-aqt.tag_ops]
|
||||
[mypy-aqt.operations.*]
|
||||
no_strict_optional = false
|
||||
|
||||
[mypy-aqt.winpaths]
|
||||
|
|
|
@ -451,7 +451,7 @@ hooks = [
|
|||
Hook(
|
||||
name="state_did_reset",
|
||||
legacy_hook="reset",
|
||||
doc="""Legacy 'reset' hook. Called by mw.reset() and mw.perform_op() to redraw the UI.
|
||||
doc="""Legacy 'reset' hook. Called by mw.reset() and CollectionOp() to redraw the UI.
|
||||
|
||||
New code should use `operation_did_execute` instead.
|
||||
""",
|
||||
|
@ -476,7 +476,7 @@ hooks = [
|
|||
),
|
||||
Hook(
|
||||
name="backend_will_block",
|
||||
doc="""Called before one or more operations are executed with mw.perform_op().
|
||||
doc="""Called before one or more DB tasks are run in the background.
|
||||
|
||||
Subscribers can use this to set a flag to avoid DB queries until the operation
|
||||
completes, as doing so will freeze the UI until the long-running operation
|
||||
|
@ -485,7 +485,7 @@ hooks = [
|
|||
),
|
||||
Hook(
|
||||
name="backend_did_block",
|
||||
doc="""Called after one or more operations are executed with mw.perform_op().
|
||||
doc="""Called after one or more DB tasks finish running in the background.
|
||||
Called regardless of the success of individual operations, and only called when
|
||||
there are no outstanding ops.
|
||||
""",
|
||||
|
|
|
@ -163,7 +163,7 @@ impl From<String> for Variable {
|
|||
let kind = match name.as_str() {
|
||||
"cards" | "notes" | "count" | "amount" | "reviews" | "total" | "selected"
|
||||
| "kilobytes" | "daysStart" | "daysEnd" | "days" | "secs-per-card" | "remaining"
|
||||
| "hourStart" | "hourEnd" | "correct" | "decks" => VariableKind::Int,
|
||||
| "hourStart" | "hourEnd" | "correct" | "decks" | "changed" => VariableKind::Int,
|
||||
"average-seconds" | "cards-per-minute" => VariableKind::Float,
|
||||
"val" | "found" | "expected" | "part" | "percent" | "day" | "number" | "up"
|
||||
| "down" | "seconds" | "megs" => VariableKind::Any,
|
||||
|
|
Loading…
Reference in a new issue