allow ops to pass metadata into perform_op()

Instances can pass handled_by=self to more easily ignore events they
initiate.

Fixes ugly refresh when expanding/collapsing decks, but we're still
refreshing the card/notes area unnecessarily in that case.
This commit is contained in:
Damien Elmes 2021-04-05 13:43:09 +10:00
parent 3adf03f9cb
commit f6ec5928ae
10 changed files with 61 additions and 33 deletions

View file

@ -24,6 +24,7 @@ from aqt.editor import Editor
from aqt.exporting import ExportDialog
from aqt.find_and_replace import FindAndReplaceDialog
from aqt.main import ResetReason
from aqt.operations import OpMeta
from aqt.operations.card import set_card_deck, set_card_flag
from aqt.operations.collection import undo
from aqt.operations.note import remove_notes
@ -127,12 +128,12 @@ class Browser(QMainWindow):
gui_hooks.browser_will_show(self)
self.show()
def on_operation_did_execute(self, changes: OpChanges) -> None:
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
focused = current_top_level_widget() == self
self.table.op_executed(changes, focused)
self.sidebar.op_executed(changes, focused)
self.table.op_executed(changes, meta, focused)
self.sidebar.op_executed(changes, meta, focused)
if changes.note or changes.notetype:
if not self.editor.is_updating_note():
if meta.handled_by is not self.editor:
# fixme: this will leave the splitter shown, but with no current
# note being edited
note = self.editor.note

View file

@ -5,6 +5,7 @@ import aqt.editor
from anki.collection import OpChanges
from anki.errors import NotFoundError
from aqt import gui_hooks
from aqt.operations import OpMeta
from aqt.qt import *
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr
@ -30,10 +31,10 @@ class EditCurrent(QDialog):
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
self.show()
def on_operation_did_execute(self, changes: OpChanges) -> None:
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
if not (changes.note or changes.notetype):
return
if self.editor.is_updating_note():
if meta.handled_by is self.editor:
return
# reload note

View file

@ -100,8 +100,8 @@ class Editor:
redrawing.
The editor will cause that hook to be fired when it saves changes. To avoid
an unwanted refresh, the parent widget should call editor.is_updating_note(),
and avoid re-setting the note if it returns true.
an unwanted refresh, the parent widget should check if meta.handled_by
corresponds to this editor instance, and ignore the change if it does.
"""
def __init__(
@ -113,7 +113,6 @@ class Editor:
self.note: Optional[Note] = None
self.addMode = addMode
self.currentField: Optional[int] = None
self._is_updating_note = False
# current card, for card layout
self.card: Optional[Card] = None
self.setupOuter()
@ -559,14 +558,7 @@ class Editor:
def _save_current_note(self) -> None:
"Call after note is updated with data from webview."
self._is_updating_note = True
update_note(mw=self.mw, note=self.note, after_hooks=self._after_updating_note)
def _after_updating_note(self) -> None:
self._is_updating_note = False
def is_updating_note(self) -> bool:
return self._is_updating_note
update_note(mw=self.mw, note=self.note, handled_by=self)
def fonts(self) -> List[Tuple[str, int, bool]]:
return [

View file

@ -61,6 +61,7 @@ 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 OpMeta
from aqt.operations.collection import undo
from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import *
@ -772,6 +773,7 @@ class AnkiQt(QMainWindow):
success: PerformOpOptionalSuccessCallback = None,
failure: PerformOpOptionalFailureCallback = None,
after_hooks: Optional[Callable[[], None]] = None,
meta: OpMeta = OpMeta(),
) -> None:
"""Run the provided operation on a background thread.
@ -825,7 +827,7 @@ class AnkiQt(QMainWindow):
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, after_hooks)
self._fire_change_hooks_after_op_performed(result, after_hooks, meta)
self.taskman.with_progress(op, wrapped_done)
@ -841,7 +843,10 @@ class AnkiQt(QMainWindow):
assert self._background_op_count >= 0
def _fire_change_hooks_after_op_performed(
self, result: ResultWithChanges, after_hooks: Optional[Callable[[], None]]
self,
result: ResultWithChanges,
after_hooks: Optional[Callable[[], None]],
meta: OpMeta,
) -> None:
if isinstance(result, OpChanges):
changes = result
@ -851,7 +856,7 @@ class AnkiQt(QMainWindow):
# fire new hook
print("op changes:")
print(changes)
gui_hooks.operation_did_execute(changes)
gui_hooks.operation_did_execute(changes, meta)
# fire legacy hook so old code notices changes
if self.col.op_made_changes(changes):
gui_hooks.state_did_reset()
@ -865,9 +870,9 @@ class AnkiQt(QMainWindow):
for field in op.DESCRIPTOR.fields:
if field.name != "kind":
setattr(op, field.name, True)
gui_hooks.operation_did_execute(op)
gui_hooks.operation_did_execute(op, None)
def on_operation_did_execute(self, changes: OpChanges) -> None:
def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None:
"Notify current screen of changes."
focused = current_top_level_widget() == self
if self.state == "review":

View file

@ -0,0 +1,16 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from dataclasses import dataclass
from typing import Optional
@dataclass
class OpMeta:
"""Metadata associated with an operation.
The `handled_by` field can be used by screens to ignore change
events they initiated themselves, if they have already made
the required changes."""
handled_by: Optional[object] = None

View file

@ -3,11 +3,12 @@
from __future__ import annotations
from typing import Callable, Sequence
from typing import Callable, Optional, Sequence
from anki.decks import DeckCollapseScope, DeckId
from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
from aqt.utils import getOnlyText, tooltip, tr
@ -71,10 +72,16 @@ def add_deck(
def set_deck_collapsed(
*, mw: AnkiQt, deck_id: DeckId, collapsed: bool, scope: DeckCollapseScope.V
*,
mw: AnkiQt,
deck_id: DeckId,
collapsed: bool,
scope: DeckCollapseScope.V,
handled_by: Optional[object] = None,
) -> None:
mw.perform_op(
lambda: mw.col.decks.set_collapsed(
deck_id=deck_id, collapsed=collapsed, scope=scope
)
),
meta=OpMeta(handled_by=handled_by),
)

View file

@ -3,12 +3,13 @@
from __future__ import annotations
from typing import Callable, Sequence
from typing import Optional, Sequence
from anki.decks import DeckId
from anki.notes import Note, NoteId
from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
def add_note(
@ -21,10 +22,10 @@ def add_note(
mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success)
def update_note(*, mw: AnkiQt, note: Note, after_hooks: Callable[[], None]) -> None:
def update_note(*, mw: AnkiQt, note: Note, handled_by: Optional[object]) -> None:
mw.perform_op(
lambda: mw.col.update_note(note),
after_hooks=after_hooks,
meta=OpMeta(handled_by=handled_by),
)

View file

@ -16,6 +16,7 @@ from anki.types import assert_exhaustive
from aqt import colors, gui_hooks
from aqt.clayout import CardLayout
from aqt.models import Models
from aqt.operations import OpMeta
from aqt.operations.deck import (
remove_decks,
rename_deck,
@ -419,7 +420,10 @@ class SidebarTreeView(QTreeView):
# Refreshing
###########################
def op_executed(self, op: OpChanges, focused: bool) -> None:
def op_executed(self, op: OpChanges, meta: OpMeta, focused: bool) -> None:
if meta.handled_by is self:
return
if op.tag or op.notetype or op.deck:
self._refresh_needed = True
if focused:
@ -980,6 +984,7 @@ class SidebarTreeView(QTreeView):
deck_id=DeckId(node.deck_id),
collapsed=not expanded,
scope=DeckCollapseScope.BROWSER,
handled_by=self,
)
for node in nodes:

View file

@ -29,6 +29,7 @@ from anki.errors import NotFoundError
from anki.notes import Note, NoteId
from anki.utils import ids2str, isWin
from aqt import colors, gui_hooks
from aqt.operations import OpMeta
from aqt.qt import *
from aqt.theme import theme_manager
from aqt.utils import (
@ -179,7 +180,7 @@ class Table:
def redraw_cells(self) -> None:
self._model.redraw_cells()
def op_executed(self, op: OpChanges, focused: bool) -> None:
def op_executed(self, op: OpChanges, meta: OpMeta, focused: bool) -> None:
print("op executed")
if op.card or op.note or op.deck or op.notetype:
self._model.empty_cache()

View file

@ -26,6 +26,7 @@ from anki.hooks import runFilter, runHook
from anki.models import NotetypeDict
from aqt.qt import QDialog, QEvent, QMenu, QWidget
from aqt.tagedit import TagEdit
import aqt.operations
"""
# Hook list
@ -458,9 +459,7 @@ hooks = [
),
Hook(
name="operation_did_execute",
args=[
"changes: anki.collection.OpChanges",
],
args=["changes: anki.collection.OpChanges", "meta: aqt.operations.OpMeta"],
doc="""Called after an operation completes.
Changes can be inspected to determine whether the UI needs updating.