diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 44a102bb7..1fe89a700 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -879,11 +879,6 @@ table.review-log {{ {revlog_style} }} assert_exhaustive(self._undo) assert False - def op_affects_study_queue(self, changes: OpChanges) -> bool: - if changes.kind == changes.SET_CARD_FLAG: - return False - return changes.card or changes.deck or changes.preference - def op_made_changes(self, changes: OpChanges) -> bool: for field in changes.DESCRIPTOR.fields: if field.name != "kind": diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 97174c2fa..2fc5ba1e2 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -24,7 +24,6 @@ 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 @@ -128,12 +127,14 @@ class Browser(QMainWindow): gui_hooks.browser_will_show(self) self.show() - def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: focused = current_top_level_widget() == self - self.table.op_executed(changes, meta, focused) - self.sidebar.op_executed(changes, meta, focused) + self.table.op_executed(changes, handler, focused) + self.sidebar.op_executed(changes, handler, focused) if changes.note or changes.notetype: - if meta.handler is not self.editor: + if handler 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/deckbrowser.py b/qt/aqt/deckbrowser.py index 34c371eb6..1d2f58ec5 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass -from typing import Any +from typing import Any, Optional import aqt from anki.collection import OpChanges @@ -76,8 +76,10 @@ class DeckBrowser: if self._refresh_needed: self.refresh() - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if self.mw.col.op_affects_study_queue(changes): + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if changes.study_queues: self._refresh_needed = True if focused: diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index 3a3a67a63..209623e37 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -1,11 +1,11 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import Optional 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 @@ -31,8 +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, meta: OpMeta) -> None: - if changes.editor and meta.handler is not self.editor: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: + if changes.editor and handler is not self.editor: # reload note note = self.editor.note try: diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 82f795994..999a8dfa6 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -100,7 +100,7 @@ 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 check if meta.handler + an unwanted refresh, the parent widget should check if handler corresponds to this editor instance, and ignore the change if it does. """ diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 6611f5cd1..7090c4a6b 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -61,7 +61,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 OpMeta from aqt.operations.collection import undo from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * @@ -773,7 +772,7 @@ class AnkiQt(QMainWindow): success: PerformOpOptionalSuccessCallback = None, failure: PerformOpOptionalFailureCallback = None, after_hooks: Optional[Callable[[], None]] = None, - meta: OpMeta = OpMeta(), + handler: Optional[object] = None, ) -> None: """Run the provided operation on a background thread. @@ -827,7 +826,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, meta) + self._fire_change_hooks_after_op_performed(result, after_hooks, handler) self.taskman.with_progress(op, wrapped_done) @@ -846,7 +845,7 @@ class AnkiQt(QMainWindow): self, result: ResultWithChanges, after_hooks: Optional[Callable[[], None]], - meta: OpMeta, + handler: Optional[object], ) -> None: if isinstance(result, OpChanges): changes = result @@ -856,7 +855,7 @@ class AnkiQt(QMainWindow): # fire new hook print("op changes:") print(changes) - gui_hooks.operation_did_execute(changes, meta) + 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() @@ -872,15 +871,17 @@ class AnkiQt(QMainWindow): setattr(op, field.name, True) gui_hooks.operation_did_execute(op, None) - def on_operation_did_execute(self, changes: OpChanges, meta: OpMeta) -> None: + def on_operation_did_execute( + self, changes: OpChanges, handler: Optional[object] + ) -> None: "Notify current screen of changes." focused = current_top_level_widget() == self if self.state == "review": - dirty = self.reviewer.op_executed(changes, focused) + dirty = self.reviewer.op_executed(changes, handler, focused) elif self.state == "overview": - dirty = self.overview.op_executed(changes, focused) + dirty = self.overview.op_executed(changes, handler, focused) elif self.state == "deckBrowser": - dirty = self.deckBrowser.op_executed(changes, focused) + dirty = self.deckBrowser.op_executed(changes, handler, focused) else: dirty = False diff --git a/qt/aqt/operations/__init__.py b/qt/aqt/operations/__init__.py index dd3d825c6..e3d8b5638 100644 --- a/qt/aqt/operations/__init__.py +++ b/qt/aqt/operations/__init__.py @@ -1,16 +1,2 @@ # 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 `handler` field can be used by screens to ignore change - events they initiated themselves, if they have already made - the required changes.""" - - handler: Optional[object] = None diff --git a/qt/aqt/operations/card.py b/qt/aqt/operations/card.py index 91c4ac0d2..fd994c475 100644 --- a/qt/aqt/operations/card.py +++ b/qt/aqt/operations/card.py @@ -3,16 +3,28 @@ from __future__ import annotations -from typing import Sequence +from typing import Optional, Sequence from anki.cards import CardId from anki.decks import DeckId from aqt import AnkiQt +from aqt.main import PerformOpOptionalSuccessCallback def set_card_deck(*, mw: AnkiQt, card_ids: Sequence[CardId], deck_id: DeckId) -> None: mw.perform_op(lambda: mw.col.set_deck(card_ids, deck_id)) -def set_card_flag(*, mw: AnkiQt, card_ids: Sequence[CardId], flag: int) -> None: - mw.perform_op(lambda: mw.col.set_user_flag_for_cards(flag, card_ids)) +def set_card_flag( + *, + mw: AnkiQt, + card_ids: Sequence[CardId], + flag: int, + handler: Optional[object] = None, + success: PerformOpOptionalSuccessCallback = None, +) -> None: + mw.perform_op( + lambda: mw.col.set_user_flag_for_cards(flag, card_ids), + handler=handler, + success=success, + ) diff --git a/qt/aqt/operations/deck.py b/qt/aqt/operations/deck.py index e32e69b0c..fc6ca6250 100644 --- a/qt/aqt/operations/deck.py +++ b/qt/aqt/operations/deck.py @@ -8,7 +8,6 @@ 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 @@ -83,5 +82,5 @@ def set_deck_collapsed( lambda: mw.col.decks.set_collapsed( deck_id=deck_id, collapsed=collapsed, scope=scope ), - meta=OpMeta(handler=handler), + handler=handler, ) diff --git a/qt/aqt/operations/note.py b/qt/aqt/operations/note.py index 4c0d8c3d2..dfe28ba92 100644 --- a/qt/aqt/operations/note.py +++ b/qt/aqt/operations/note.py @@ -9,7 +9,6 @@ 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( @@ -25,7 +24,7 @@ def add_note( def update_note(*, mw: AnkiQt, note: Note, handler: Optional[object]) -> None: mw.perform_op( lambda: mw.col.update_note(note), - meta=OpMeta(handler=handler), + handler=handler, ) diff --git a/qt/aqt/operations/tag.py b/qt/aqt/operations/tag.py index ee1e767cb..49b38cb2c 100644 --- a/qt/aqt/operations/tag.py +++ b/qt/aqt/operations/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Callable, Sequence +from typing import Callable, Optional, Sequence from anki.collection import OpChangesWithCount from anki.notes import NoteId @@ -18,9 +18,12 @@ def add_tags_to_notes( note_ids: Sequence[NoteId], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, + handler: Optional[object] = None, ) -> None: mw.perform_op( - lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), success=success + lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), + success=success, + handler=handler, ) @@ -30,9 +33,12 @@ def remove_tags_from_notes( note_ids: Sequence[NoteId], space_separated_tags: str, success: PerformOpOptionalSuccessCallback = None, + handler: Optional[object] = None, ) -> None: mw.perform_op( - lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), success=success + lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), + success=success, + handler=handler, ) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 8d2df0d76..2e7e01ea7 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -64,8 +64,10 @@ class Overview: if self._refresh_needed: self.refresh() - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if self.mw.col.op_affects_study_queue(changes): + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if changes.study_queues: self._refresh_needed = True if focused: diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index feb9d8e4d..3f665d1a4 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -14,7 +14,7 @@ from PyQt5.QtCore import Qt from anki import hooks from anki.cards import Card, CardId -from anki.collection import Config, OpChanges +from anki.collection import Config, OpChanges, OpChangesWithCount from anki.tags import MARKED_TAG from anki.utils import stripHTML from aqt import AnkiQt, gui_hooks @@ -38,7 +38,6 @@ from aqt.webview import AnkiWebView class RefreshNeeded(Enum): - NO = auto() NOTE_TEXT = auto() QUEUES = auto() @@ -71,7 +70,7 @@ class Reviewer: self._recordedAudio: Optional[str] = None self.typeCorrect: str = None # web init happens before this is set self.state: Optional[str] = None - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed: Optional[RefreshNeeded] = None self.bottom = BottomBar(mw, mw.bottomWeb) hooks.card_did_leech.append(self.onLeech) @@ -102,29 +101,25 @@ class Reviewer: self.mw.col.reset() self.nextCard() self.mw.fade_in_webview() - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed = None elif self._refresh_needed is RefreshNeeded.NOTE_TEXT: self._redraw_current_card() self.mw.fade_in_webview() - self._refresh_needed = RefreshNeeded.NO + self._refresh_needed = None - def op_executed(self, changes: OpChanges, focused: bool) -> bool: - if changes.note and changes.kind == OpChanges.UPDATE_NOTE_TAGS: - self.card.load() - self._update_mark_icon() - elif changes.card and changes.kind == OpChanges.SET_CARD_FLAG: - # fixme: v3 mtime check - self.card.load() - self._update_flag_icon() - elif self.mw.col.op_affects_study_queue(changes): - self._refresh_needed = RefreshNeeded.QUEUES - elif changes.note or changes.notetype or changes.tag: - self._refresh_needed = RefreshNeeded.NOTE_TEXT + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> bool: + if handler is not self: + if changes.study_queues: + self._refresh_needed = RefreshNeeded.QUEUES + elif changes.editor: + self._refresh_needed = RefreshNeeded.NOTE_TEXT - if focused and self._refresh_needed is not RefreshNeeded.NO: + if focused and self._refresh_needed: self.refresh_if_needed() - return self._refresh_needed is not RefreshNeeded.NO + return bool(self._refresh_needed) def _redraw_current_card(self) -> None: self.card.load() @@ -830,23 +825,45 @@ time = %(time)d; self.mw.onDeckConf(self.mw.col.decks.get(self.card.current_deck_id())) def set_flag_on_current_card(self, desired_flag: int) -> None: + def redraw_flag(out: OpChanges) -> None: + self.card.load() + self._update_flag_icon() + # need to toggle off? if self.card.user_flag() == desired_flag: flag = 0 else: flag = desired_flag - set_card_flag(mw=self.mw, card_ids=[self.card.id], flag=flag) + set_card_flag( + mw=self.mw, + card_ids=[self.card.id], + flag=flag, + handler=self, + success=redraw_flag, + ) def toggle_mark_on_current_note(self) -> None: + def redraw_mark(out: OpChangesWithCount) -> None: + self.card.load() + self._update_mark_icon() + note = self.card.note() if note.has_tag(MARKED_TAG): remove_tags_from_notes( - mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG + mw=self.mw, + note_ids=[note.id], + space_separated_tags=MARKED_TAG, + handler=self, + success=redraw_mark, ) else: add_tags_to_notes( - mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG + mw=self.mw, + note_ids=[note.id], + space_separated_tags=MARKED_TAG, + handler=self, + success=redraw_mark, ) def on_set_due(self) -> None: diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f8d83c354..e9b9b1d1c 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -16,7 +16,6 @@ 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, @@ -420,8 +419,10 @@ class SidebarTreeView(QTreeView): # Refreshing ########################### - def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None: - if changes.browser_sidebar and not meta.handler is self: + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> None: + if changes.browser_sidebar and not handler is self: self._refresh_needed = True if focused: self.refresh_if_needed() diff --git a/qt/aqt/table.py b/qt/aqt/table.py index aec9d7b71..cc94b8f44 100644 --- a/qt/aqt/table.py +++ b/qt/aqt/table.py @@ -29,7 +29,6 @@ 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 ( @@ -181,7 +180,9 @@ class Table: def redraw_cells(self) -> None: self._model.redraw_cells() - def op_executed(self, changes: OpChanges, meta: OpMeta, focused: bool) -> None: + def op_executed( + self, changes: OpChanges, handler: Optional[object], focused: bool + ) -> None: if changes.browser_table: self._model.mark_cache_stale() if focused: diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index ebcf373b1..a939112cf 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -459,7 +459,7 @@ hooks = [ ), Hook( name="operation_did_execute", - args=["changes: anki.collection.OpChanges", "meta: aqt.operations.OpMeta"], + args=["changes: anki.collection.OpChanges", "handler: Optional[object]"], doc="""Called after an operation completes. Changes can be inspected to determine whether the UI needs updating. diff --git a/rslib/backend.proto b/rslib/backend.proto index 06fe920e1..e5430c48d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1498,26 +1498,17 @@ message GetQueuedCardsOut { } message OpChanges { - // this is not an exhaustive list; we can add more cases as we need them - enum Kind { - OTHER = 0; - UPDATE_NOTE_TAGS = 1; - SET_CARD_FLAG = 2; - UPDATE_NOTE = 3; - } + bool card = 1; + bool note = 2; + bool deck = 3; + bool tag = 4; + bool notetype = 5; + bool preference = 6; - Kind kind = 1; - bool card = 2; - bool note = 3; - bool deck = 4; - bool tag = 5; - bool notetype = 6; - bool preference = 7; - - bool browser_table = 8; - bool browser_sidebar = 9; - bool editor = 10; - bool study_queues = 11; + bool browser_table = 7; + bool browser_sidebar = 8; + bool editor = 9; + bool study_queues = 10; } message UndoStatus { diff --git a/rslib/src/backend/ops.rs b/rslib/src/backend/ops.rs index d634093ab..001357923 100644 --- a/rslib/src/backend/ops.rs +++ b/rslib/src/backend/ops.rs @@ -1,8 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use pb::op_changes::Kind; - use crate::{ backend_proto as pb, ops::OpChanges, @@ -10,21 +8,9 @@ use crate::{ undo::{UndoOutput, UndoStatus}, }; -impl From for Kind { - fn from(o: Op) -> Self { - match o { - Op::SetFlag => Kind::SetCardFlag, - Op::UpdateTag => Kind::UpdateNoteTags, - Op::UpdateNote => Kind::UpdateNote, - _ => Kind::Other, - } - } -} - impl From for pb::OpChanges { fn from(c: OpChanges) -> Self { pb::OpChanges { - kind: Kind::from(c.op) as i32, card: c.changes.card, note: c.changes.note, deck: c.changes.deck,