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

View file

@ -5,6 +5,7 @@ import aqt.editor
from anki.collection import OpChanges from anki.collection import OpChanges
from anki.errors import NotFoundError from anki.errors import NotFoundError
from aqt import gui_hooks from aqt import gui_hooks
from aqt.operations import OpMeta
from aqt.qt import * from aqt.qt import *
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr 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) gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
self.show() 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): if not (changes.note or changes.notetype):
return return
if self.editor.is_updating_note(): if meta.handled_by is self.editor:
return return
# reload note # reload note

View file

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

View file

@ -61,6 +61,7 @@ from aqt.emptycards import show_empty_cards
from aqt.legacy import install_pylib_legacy from aqt.legacy import install_pylib_legacy
from aqt.mediacheck import check_media_db from aqt.mediacheck import check_media_db
from aqt.mediasync import MediaSyncer from aqt.mediasync import MediaSyncer
from aqt.operations import OpMeta
from aqt.operations.collection import undo from aqt.operations.collection import undo
from aqt.profiles import ProfileManager as ProfileManagerType from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
@ -772,6 +773,7 @@ class AnkiQt(QMainWindow):
success: PerformOpOptionalSuccessCallback = None, success: PerformOpOptionalSuccessCallback = None,
failure: PerformOpOptionalFailureCallback = None, failure: PerformOpOptionalFailureCallback = None,
after_hooks: Optional[Callable[[], None]] = None, after_hooks: Optional[Callable[[], None]] = None,
meta: OpMeta = OpMeta(),
) -> None: ) -> None:
"""Run the provided operation on a background thread. """Run the provided operation on a background thread.
@ -825,7 +827,7 @@ class AnkiQt(QMainWindow):
status = self.col.undo_status() status = self.col.undo_status()
self._update_undo_actions_for_status_and_save(status) self._update_undo_actions_for_status_and_save(status)
# fire change hooks # 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) self.taskman.with_progress(op, wrapped_done)
@ -841,7 +843,10 @@ class AnkiQt(QMainWindow):
assert self._background_op_count >= 0 assert self._background_op_count >= 0
def _fire_change_hooks_after_op_performed( 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: ) -> None:
if isinstance(result, OpChanges): if isinstance(result, OpChanges):
changes = result changes = result
@ -851,7 +856,7 @@ class AnkiQt(QMainWindow):
# fire new hook # fire new hook
print("op changes:") print("op changes:")
print(changes) print(changes)
gui_hooks.operation_did_execute(changes) gui_hooks.operation_did_execute(changes, meta)
# fire legacy hook so old code notices changes # fire legacy hook so old code notices changes
if self.col.op_made_changes(changes): if self.col.op_made_changes(changes):
gui_hooks.state_did_reset() gui_hooks.state_did_reset()
@ -865,9 +870,9 @@ class AnkiQt(QMainWindow):
for field in op.DESCRIPTOR.fields: for field in op.DESCRIPTOR.fields:
if field.name != "kind": if field.name != "kind":
setattr(op, field.name, True) 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." "Notify current screen of changes."
focused = current_top_level_widget() == self focused = current_top_level_widget() == self
if self.state == "review": 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 __future__ import annotations
from typing import Callable, Sequence from typing import Callable, Optional, Sequence
from anki.decks import DeckCollapseScope, DeckId from anki.decks import DeckCollapseScope, DeckId
from aqt import AnkiQt, QWidget from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
from aqt.utils import getOnlyText, tooltip, tr from aqt.utils import getOnlyText, tooltip, tr
@ -71,10 +72,16 @@ def add_deck(
def set_deck_collapsed( 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: ) -> None:
mw.perform_op( mw.perform_op(
lambda: mw.col.decks.set_collapsed( lambda: mw.col.decks.set_collapsed(
deck_id=deck_id, collapsed=collapsed, scope=scope 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 __future__ import annotations
from typing import Callable, Sequence from typing import Optional, Sequence
from anki.decks import DeckId from anki.decks import DeckId
from anki.notes import Note, NoteId from anki.notes import Note, NoteId
from aqt import AnkiQt from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import OpMeta
def add_note( def add_note(
@ -21,10 +22,10 @@ def add_note(
mw.perform_op(lambda: mw.col.add_note(note, target_deck_id), success=success) 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( mw.perform_op(
lambda: mw.col.update_note(note), 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 import colors, gui_hooks
from aqt.clayout import CardLayout from aqt.clayout import CardLayout
from aqt.models import Models from aqt.models import Models
from aqt.operations import OpMeta
from aqt.operations.deck import ( from aqt.operations.deck import (
remove_decks, remove_decks,
rename_deck, rename_deck,
@ -419,7 +420,10 @@ class SidebarTreeView(QTreeView):
# Refreshing # 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: if op.tag or op.notetype or op.deck:
self._refresh_needed = True self._refresh_needed = True
if focused: if focused:
@ -980,6 +984,7 @@ class SidebarTreeView(QTreeView):
deck_id=DeckId(node.deck_id), deck_id=DeckId(node.deck_id),
collapsed=not expanded, collapsed=not expanded,
scope=DeckCollapseScope.BROWSER, scope=DeckCollapseScope.BROWSER,
handled_by=self,
) )
for node in nodes: for node in nodes:

View file

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

View file

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