move deck/notetype update hooks to gui

We need to migrate away from firing hooks in libanki, since libanki
methods may be running on a background thread, and hook consumers
typically expect the code to run in the main thread. We could document
it, but it would frequently be forgotten about, and could lead to
crashes.

https://anki.tenderapp.com/discussions/ankidesktop/41748-qobject-cannot-create-children-for-a-parent-that-is-in-a-different-thread-when-hitting-the-save-button-on-clayoutpy-window
This commit is contained in:
Damien Elmes 2020-05-22 10:47:14 +10:00
parent 16eceb5260
commit 9baa8530d5
9 changed files with 72 additions and 29 deletions

View file

@ -8,7 +8,6 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb
from anki import hooks
from anki.consts import *
from anki.errors import DeckRenameError
from anki.lang import _
@ -96,8 +95,6 @@ class DeckManager:
deck["name"] = name
self.update(deck, preserve_usn=False)
hooks.deck_added(deck)
return deck["id"]
def rem(self, did: int, cardsToo: bool = True, childrenToo: bool = True) -> None:

View file

@ -188,6 +188,8 @@ card_will_flush = _CardWillFlushHook()
class _DeckAddedHook:
"""Obsolete, do not use."""
_hooks: List[Callable[[Dict[str, Any]], None]] = []
def append(self, cb: Callable[[Dict[str, Any]], None]) -> None:
@ -206,8 +208,6 @@ class _DeckAddedHook:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
# legacy support
runHook("newDeck")
deck_added = _DeckAddedHook()
@ -306,6 +306,8 @@ media_files_did_export = _MediaFilesDidExportHook()
class _NoteTypeAddedHook:
"""Obsolete, do not use."""
_hooks: List[Callable[[Dict[str, Any]], None]] = []
def append(self, cb: Callable[[Dict[str, Any]], None]) -> None:
@ -324,8 +326,6 @@ class _NoteTypeAddedHook:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
# legacy support
runHook("newModel")
note_type_added = _NoteTypeAddedHook()

View file

@ -9,7 +9,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb
from anki import hooks
from anki.consts import *
from anki.lang import _
from anki.rsbackend import StockNoteType
@ -81,9 +80,6 @@ class ModelManager:
self.update(m, preserve_usn=False)
# fixme: badly named; also fires on updates
hooks.note_type_added(m)
# legacy
def flush(self) -> None:
pass

View file

@ -8,6 +8,8 @@ To add a new hook:
- update the hooks list below
- run 'make develop'
- send a pull request that includes the changes to this file and hooks.py
In most cases, hooks are better placed in genhooks_gui.py
"""
import os
@ -25,24 +27,12 @@ hooks = [
args=["col: anki.collection.Collection", "ids: List[int]"],
legacy_hook="remNotes",
),
Hook(
name="deck_added",
args=["deck: Dict[str, Any]"],
legacy_hook="newDeck",
legacy_no_args=True,
),
Hook(name="media_files_did_export", args=["count: int"]),
Hook(
name="exporters_list_created",
args=["exporters: List[Tuple[str, Any]]"],
legacy_hook="exportersList",
),
Hook(
name="note_type_added",
args=["notetype: Dict[str, Any]"],
legacy_hook="newModel",
legacy_no_args=True,
),
Hook(name="sync_stage_did_change", args=["stage: str"], legacy_hook="sync"),
Hook(name="sync_progress_did_change", args=["msg: str"], legacy_hook="syncMsg"),
Hook(
@ -101,6 +91,13 @@ hooks = [
doc="""Allows changing the number of rev card for this deck (without
considering descendants).""",
),
# obsolete
Hook(name="deck_added", args=["deck: Dict[str, Any]"], doc="Obsolete, do not use."),
Hook(
name="note_type_added",
args=["notetype: Dict[str, Any]"],
doc="Obsolete, do not use.",
),
]
if __name__ == "__main__":

View file

@ -14,7 +14,6 @@ from typing import Callable, List, Optional, Sequence, Union
import anki
import aqt
import aqt.forms
from anki import hooks
from anki.cards import Card
from anki.collection import Collection
from anki.consts import *
@ -1879,8 +1878,8 @@ update cards set usn=?, mod=?, did=? where id in """
gui_hooks.editor_did_fire_typing_timer.append(self.refreshCurrentCard)
gui_hooks.editor_did_load_note.append(self.onLoadNote)
gui_hooks.editor_did_unfocus_field.append(self.on_unfocus_field)
hooks.note_type_added.append(self.on_item_added)
hooks.deck_added.append(self.on_item_added)
gui_hooks.sidebar_should_refresh_decks.append(self.on_item_added)
gui_hooks.sidebar_should_refresh_notetypes.append(self.on_item_added)
def teardownHooks(self) -> None:
gui_hooks.undo_state_did_change.remove(self.onUndoState)
@ -1888,14 +1887,14 @@ update cards set usn=?, mod=?, did=? where id in """
gui_hooks.editor_did_fire_typing_timer.remove(self.refreshCurrentCard)
gui_hooks.editor_did_load_note.remove(self.onLoadNote)
gui_hooks.editor_did_unfocus_field.remove(self.on_unfocus_field)
hooks.note_type_added.remove(self.on_item_added)
hooks.deck_added.remove(self.on_item_added)
gui_hooks.sidebar_should_refresh_decks.remove(self.on_item_added)
gui_hooks.sidebar_should_refresh_notetypes.remove(self.on_item_added)
def on_unfocus_field(self, changed: bool, note: Note, field_idx: int) -> None:
self.refreshCurrentCard(note)
# covers the tag, note and deck case
def on_item_added(self, item: Any) -> None:
def on_item_added(self, item: Any = None) -> None:
self.maybeRefreshSidebar()
def on_tag_list_update(self):

View file

@ -749,6 +749,7 @@ Enter deck to place new %s cards in, or leave blank:"""
self.mw.reset()
tooltip("Changes saved.", parent=self.parent())
self.cleanup()
gui_hooks.sidebar_should_refresh_notetypes()
return QDialog.accept(self)
self.mw.taskman.with_progress(save, on_done)

View file

@ -82,6 +82,7 @@ class DeckBrowser:
deck = getOnlyText(_("Name for deck:"))
if deck:
self.mw.col.decks.id(deck)
gui_hooks.sidebar_should_refresh_decks()
self.refresh()
elif cmd == "drag":
draggedDeckDid, ontoDeckDid = arg.split(",")
@ -256,6 +257,7 @@ where id > ?""",
return
try:
self.mw.col.decks.rename(deck, newName)
gui_hooks.sidebar_should_refresh_decks()
except DeckRenameError as e:
return showWarning(e.description)
self.show()
@ -276,6 +278,7 @@ where id > ?""",
def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid):
try:
self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid)
gui_hooks.sidebar_should_refresh_decks()
except DeckRenameError as e:
return showWarning(e.description)

View file

@ -1774,6 +1774,54 @@ class _ReviewerWillShowContextMenuHook:
reviewer_will_show_context_menu = _ReviewerWillShowContextMenuHook()
class _SidebarShouldRefreshDecksHook:
_hooks: List[Callable[[], None]] = []
def append(self, cb: Callable[[], None]) -> None:
"""()"""
self._hooks.append(cb)
def remove(self, cb: Callable[[], None]) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(self) -> None:
for hook in self._hooks:
try:
hook()
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
sidebar_should_refresh_decks = _SidebarShouldRefreshDecksHook()
class _SidebarShouldRefreshNotetypesHook:
_hooks: List[Callable[[], None]] = []
def append(self, cb: Callable[[], None]) -> None:
"""()"""
self._hooks.append(cb)
def remove(self, cb: Callable[[], None]) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(self) -> None:
for hook in self._hooks:
try:
hook()
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
sidebar_should_refresh_notetypes = _SidebarShouldRefreshNotetypesHook()
class _StateDidChangeHook:
_hooks: List[Callable[[str, str], None]] = []

View file

@ -544,6 +544,8 @@ hooks = [
legacy_hook="currentModelChanged",
legacy_no_args=True,
),
Hook(name="sidebar_should_refresh_decks"),
Hook(name="sidebar_should_refresh_notetypes"),
Hook(
name="deck_browser_will_show_options_menu",
args=["menu: QMenu", "deck_id: int"],