diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 19c8c62e3..dbe73eb20 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -13,7 +13,6 @@ import anki # pylint: disable=unused-import from anki import hooks from anki.consts import * from anki.errors import DeckRenameError -from anki.hooks import runHook from anki.lang import _ from anki.utils import ids2str, intTime @@ -167,8 +166,6 @@ class DeckManager: self.save(g) self.maybeAddToActive() hooks.run_deck_created_hook(g) - # legacy hook did not pass deck - runHook("newDeck") return int(id) def rem(self, did: int, cardsToo: bool = False, childrenToo: bool = True) -> None: diff --git a/pylib/anki/hooks.py b/pylib/anki/hooks.py index 1204fdbc2..b15712e46 100644 --- a/pylib/anki/hooks.py +++ b/pylib/anki/hooks.py @@ -54,6 +54,7 @@ rendered_card_template_filter: List[ str, ] ] = [] +sync_progress_message_hook: List[Callable[[str], None]] = [] sync_stage_hook: List[Callable[[str], None]] = [] tag_created_hook: List[Callable[[str], None]] = [] @@ -78,6 +79,8 @@ def run_deck_created_hook(deck: Dict[str, Any]) -> None: # if the hook fails, remove it deck_created_hook.remove(hook) raise + # legacy support + runHook("newDeck") def run_exported_media_files_hook(count: int) -> None: @@ -166,6 +169,8 @@ def run_note_type_created_hook(notetype: Dict[str, Any]) -> None: # if the hook fails, remove it note_type_created_hook.remove(hook) raise + # legacy support + runHook("newModel") def run_odue_invalid_hook() -> None: @@ -222,6 +227,18 @@ def run_rendered_card_template_filter( return text +def run_sync_progress_message_hook(msg: str) -> None: + for hook in sync_progress_message_hook: + try: + hook(msg) + except: + # if the hook fails, remove it + sync_progress_message_hook.remove(hook) + raise + # legacy support + runHook("syncMsg", msg) + + def run_sync_stage_hook(stage: str) -> None: for hook in sync_stage_hook: try: @@ -230,6 +247,8 @@ def run_sync_stage_hook(stage: str) -> None: # if the hook fails, remove it sync_stage_hook.remove(hook) raise + # legacy support + runHook("sync", stage) def run_tag_created_hook(tag: str) -> None: @@ -240,6 +259,8 @@ def run_tag_created_hook(tag: str) -> None: # if the hook fails, remove it tag_created_hook.remove(hook) raise + # legacy support + runHook("newTag") # @@AUTOGEN@@ diff --git a/pylib/anki/hooks_gen.py b/pylib/anki/hooks_gen.py index 8397f81f2..c1eb33b75 100644 --- a/pylib/anki/hooks_gen.py +++ b/pylib/anki/hooks_gen.py @@ -22,6 +22,8 @@ class Hook: return_type: Optional[str] = None # if add-ons may be relying on the legacy hook name, add it here legacy_hook: Optional[str] = None + # if legacy hook takes no arguments but the new hook does, set this + legacy_no_args: bool = False def callable(self) -> str: "Convert args into a Callable." @@ -63,6 +65,13 @@ class Hook: # hook return self.hook_fire_code() + def legacy_args(self) -> str: + if self.legacy_no_args: + # hook name only + return f'"{self.legacy_hook}"' + else: + return ", ".join([f'"{self.legacy_hook}"'] + self.arg_names()) + def hook_fire_code(self) -> str: arg_names = self.arg_names() out = f"""\ @@ -76,10 +85,9 @@ def run_{self.full_name()}({", ".join(self.args or [])}) -> None: raise """ if self.legacy_hook: - args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) out += f"""\ # legacy support - runHook({args}) + runHook({self.legacy_args()}) """ return out + "\n\n" @@ -96,10 +104,9 @@ def run_{self.full_name()}({", ".join(self.args or [])}) -> {self.return_type}: raise """ if self.legacy_hook: - args = ", ".join([f'"{self.legacy_hook}"'] + arg_names) out += f"""\ # legacy support - runFilter({args}) + runFilter({self.legacy_args()}) """ out += f"""\ diff --git a/pylib/anki/models.py b/pylib/anki/models.py index b5d1a04bd..fd5c18286 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -12,7 +12,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import anki # pylint: disable=unused-import from anki import hooks from anki.consts import * -from anki.hooks import runHook from anki.lang import _ from anki.types import Field, NoteType, Template from anki.utils import checksum, ids2str, intTime, joinFields, splitFields @@ -109,8 +108,6 @@ class ModelManager: self._syncTemplates(m) self.changed = True hooks.run_note_type_created_hook(m) - # legacy hook did not pass note type - runHook("newModel") def flush(self) -> None: "Flush the registry if any models were changed." diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 322d56b6c..278ed9385 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -19,7 +19,6 @@ from anki.db import DB, DBError from anki.utils import checksum, devMode, ids2str, intTime, platDesc, versionWithBuild from . import hooks -from .hooks import runHook from .lang import ngettext # syncing vars @@ -836,8 +835,7 @@ class MediaSyncer: if not fnames: break - runHook( - "syncMsg", + hooks.run_sync_progress_message_hook( ngettext( "%d media change to upload", "%d media changes to upload", toSend ) @@ -888,8 +886,7 @@ class MediaSyncer: fnames = fnames[cnt:] n = self.downloadCount - runHook( - "syncMsg", + hooks.run_sync_progress_message_hook( ngettext("%d media file downloaded", "%d media files downloaded", n) % n, ) diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 09448101f..314e1abf9 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -17,7 +17,6 @@ from typing import Callable, Dict, List, Tuple import anki # pylint: disable=unused-import from anki import hooks -from anki.hooks import runHook from anki.utils import ids2str, intTime @@ -52,7 +51,6 @@ class TagManager: self.changed = True if found: hooks.run_tag_created_hook(t) # pylint: disable=undefined-loop-variable - runHook("newTag") def all(self) -> List: return list(self.tags.keys()) diff --git a/pylib/tools/genhooks.py b/pylib/tools/genhooks.py index 33f3fa724..e55e6d03f 100644 --- a/pylib/tools/genhooks.py +++ b/pylib/tools/genhooks.py @@ -25,7 +25,12 @@ hooks = [ args=["col: anki.storage._Collection", "ids: List[int]"], legacy_hook="remNotes", ), - Hook(name="deck_created", args=["deck: Dict[str, Any]"]), + Hook( + name="deck_created", + args=["deck: Dict[str, Any]"], + legacy_hook="newDeck", + legacy_no_args=True, + ), Hook(name="exported_media_files", args=["count: int"]), Hook( name="create_exporters_list", @@ -37,11 +42,19 @@ hooks = [ args=["searches: Dict[str, Callable]"], legacy_hook="search", ), - Hook(name="note_type_created", args=["notetype: Dict[str, Any]"]), - Hook(name="sync_stage", args=["stage: str"]), + Hook( + name="note_type_created", + args=["notetype: Dict[str, Any]"], + legacy_hook="newModel", + legacy_no_args=True, + ), + Hook(name="sync_stage", args=["stage: str"], legacy_hook="sync"), + Hook(name="sync_progress_message", args=["msg: str"], legacy_hook="syncMsg"), Hook(name="http_data_sent", args=["bytes: int"]), Hook(name="http_data_received", args=["bytes: int"]), - Hook(name="tag_created", args=["tag: str"]), + Hook( + name="tag_created", args=["tag: str"], legacy_hook="newTag", legacy_no_args=True + ), Hook( name="modify_fields_for_rendering", args=["fields: Dict[str, str]", "notetype: Dict[str, Any]", "data: QAData",], diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 25a524e37..f8da732b7 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -9,6 +9,7 @@ from __future__ import annotations from typing import Any, Callable, Dict, List # pylint: disable=unused-import +from anki.cards import Card from anki.hooks import runFilter, runHook # pylint: disable=unused-import # New hook/filter handling @@ -20,6 +21,8 @@ from anki.hooks import runFilter, runHook # pylint: disable=unused-import mpv_idle_hook: List[Callable[[], None]] = [] mpv_will_play_hook: List[Callable[[str], None]] = [] +reviewer_showing_answer_hook: List[Callable[[Card], None]] = [] +reviewer_showing_question_hook: List[Callable[[Card], None]] = [] def run_mpv_idle_hook() -> None: @@ -44,4 +47,28 @@ def run_mpv_will_play_hook(file: str) -> None: runHook("mpvWillPlay", file) +def run_reviewer_showing_answer_hook(card: Card) -> None: + for hook in reviewer_showing_answer_hook: + try: + hook(card) + except: + # if the hook fails, remove it + reviewer_showing_answer_hook.remove(hook) + raise + # legacy support + runHook("showAnswer") + + +def run_reviewer_showing_question_hook(card: Card) -> None: + for hook in reviewer_showing_question_hook: + try: + hook(card) + except: + # if the hook fails, remove it + reviewer_showing_question_hook.remove(hook) + raise + # legacy support + runHook("showQuestion") + + # @@AUTOGEN@@ diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 45fb88b1a..9ece39bc5 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -16,7 +16,7 @@ from anki.cards import Card from anki.hooks import runFilter, runHook from anki.lang import _, ngettext from anki.utils import bodyClass, stripHTML -from aqt import AnkiQt +from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.sound import clearAudioQueue, getAudio, play, playFromText from aqt.utils import ( @@ -200,7 +200,7 @@ The front of this card is empty. Please run Tools>Empty Cards.""" if self.typeCorrect: self.mw.web.setFocus() # user hook - runHook("showQuestion") + gui_hooks.run_reviewer_showing_question_hook(c) def autoplay(self, card): return self.mw.col.decks.confForDid(card.odid or card.did)["autoplay"] @@ -235,7 +235,7 @@ The front of this card is empty. Please run Tools>Empty Cards.""" self.web.eval("_showAnswer(%s);" % json.dumps(a)) self._showEaseButtons() # user hook - runHook("showAnswer") + gui_hooks.run_reviewer_showing_answer_hook(c) # Answering a card ############################################################ diff --git a/qt/tools/genhooks.py b/qt/tools/genhooks.py index 82ed0b547..294fd8117 100644 --- a/qt/tools/genhooks.py +++ b/qt/tools/genhooks.py @@ -15,6 +15,18 @@ from anki.hooks_gen import Hook, update_file hooks = [ Hook(name="mpv_idle"), Hook(name="mpv_will_play", args=["file: str"], legacy_hook="mpvWillPlay"), + Hook( + name="reviewer_showing_question", + args=["card: Card"], + legacy_hook="showQuestion", + legacy_no_args=True, + ), + Hook( + name="reviewer_showing_answer", + args=["card: Card"], + legacy_hook="showAnswer", + legacy_no_args=True, + ), ] if __name__ == "__main__":