diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index a2394ca2e..f365c6a89 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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 diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 1d159dcd4..cde0b0f1f 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -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 diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 4278cc71d..c1cea825e 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -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 [ diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 140bb7317..6611f5cd1 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -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": diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index e69de29bb..49e7bdc55 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -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 diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index 92fd3f1d6..ff934c908 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -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), ) diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index 31b769954..1e3bb41d0 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -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), ) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 9ae83e495..f362474a0 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -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: diff --git a/qt/aqt/table.py b/qt/aqt/table.py index 2e41d37e9..dd6983b10 100644 --- a/qt/aqt/table.py +++ b/qt/aqt/table.py @@ -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() diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 7a268c97d..ebcf373b1 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -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.