diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 0194c3296..7c0d171d2 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -12,6 +12,7 @@ from anki.lang import _ from anki.notes import Note from anki.utils import htmlToTextLine, isMac from aqt import AnkiQt, gui_hooks +from aqt.main import ResetReason from aqt.qt import * from aqt.sound import av_player from aqt.utils import ( @@ -177,8 +178,8 @@ class AddCards(QDialog): self.mw.col.add_note(note, self.deckChooser.selectedId()) self.mw.col.clearUndo() self.addHistory(note) - self.mw.requireReset() self.previousNote = note + self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self) gui_hooks.add_cards_did_add_note(note) return note diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 9f1865366..5bde1ccf2 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -26,6 +26,7 @@ from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin from aqt import AnkiQt, gui_hooks from aqt.editor import Editor from aqt.exporting import ExportDialog +from aqt.main import ResetReason from aqt.previewer import BrowserPreviewer as PreviewDialog from aqt.previewer import Previewer from aqt.qt import * @@ -1616,7 +1617,7 @@ update cards set usn=?, mod=?, did=? where id in """ did, ) self.model.endReset() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self) # Tags ###################################################################### @@ -1642,7 +1643,7 @@ update cards set usn=?, mod=?, did=? where id in """ self.model.beginReset() func(self.selectedNotes(), tags) self.model.endReset() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self) def deleteTags(self, tags=None, label=None): if label is None: @@ -1675,7 +1676,7 @@ update cards set usn=?, mod=?, did=? where id in """ else: self.col.sched.unsuspendCards(c) self.model.reset() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self) # Exporting ###################################################################### @@ -1763,7 +1764,7 @@ update cards set usn=?, mod=?, did=? where id in """ shift=frm.shift.isChecked(), ) self.search() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserReposition, context=self) self.model.endReset() # Rescheduling @@ -1789,7 +1790,7 @@ update cards set usn=?, mod=?, did=? where id in """ fmax = max(fmin, fmax) self.col.sched.reschedCards(self.selectedCards(), fmin, fmax) self.search() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserReschedule, context=self) self.model.endReset() # Edit: selection @@ -1923,7 +1924,7 @@ update cards set usn=?, mod=?, did=? where id in """ def on_done(fut): self.search() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserFindReplace, context=self) self.model.endReset() total = len(nids) @@ -2025,7 +2026,7 @@ update cards set usn=?, mod=?, did=? where id in """ self.col.tags.bulkAdd(list(nids), _("duplicate")) self.mw.progress.finish() self.model.endReset() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self) tooltip(_("Notes tagged.")) def dupeLinkClicked(self, link): diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index e612f6c65..0d0e82ce9 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -5,6 +5,7 @@ import aqt.editor from anki.lang import _ from aqt import gui_hooks +from aqt.main import ResetReason from aqt.qt import * from aqt.utils import restoreGeom, saveGeom, tooltip @@ -27,7 +28,7 @@ class EditCurrent(QDialog): self.editor.setNote(self.mw.reviewer.card.note(), focusTo=0) restoreGeom(self, "editcurrent") gui_hooks.state_did_reset.append(self.onReset) - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.EditCurrentInit, context=self) self.show() # reset focus after open, taking care not to retain webview # pylint: disable=unnecessary-lambda diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 5ffc57981..5f2c5e8de 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -26,6 +26,7 @@ from anki.lang import _ from anki.notes import Note from anki.utils import checksum, isLin, isWin, namedtmp, stripHTMLMedia from aqt import AnkiQt, gui_hooks +from aqt.main import ResetReason from aqt.qt import * from aqt.sound import av_player, getAudio from aqt.theme import theme_manager @@ -393,7 +394,7 @@ class Editor: if not self.addMode: self.note.flush() - self.mw.requireReset() + self.mw.requireReset(reason=ResetReason.EditorBridgeCmd, context=self) if type == "blur": self.currentField = None # run any filters diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 71aec7506..535f3c595 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -7,7 +7,7 @@ See pylib/anki/hooks.py from __future__ import annotations -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable, List, Optional, Tuple, Union import anki import aqt @@ -1737,6 +1737,57 @@ class _MainWindowDidInitHook: main_window_did_init = _MainWindowDidInitHook() +class _MainWindowShouldRequireResetFilter: + """Executed before the main window will require a reset + + This hook can be used to change the behavior of the main window, + when other dialogs, like the AddCards or Browser, require a reset + from the main window. + If you decide to use this hook, make you sure you check the reason for the reset. + Some reasons require more attention than others, and skipping important ones might + put the main window into an invalid state (e.g. display a deleted note). + """ + + _hooks: List[ + Callable[[bool, "Union[aqt.main.ResetReason, str]", Optional[Any]], bool] + ] = [] + + def append( + self, + cb: Callable[[bool, "Union[aqt.main.ResetReason, str]", Optional[Any]], bool], + ) -> None: + """(will_reset: bool, reason: Union[aqt.main.ResetReason, str], context: Optional[Any])""" + self._hooks.append(cb) + + def remove( + self, + cb: Callable[[bool, "Union[aqt.main.ResetReason, str]", Optional[Any]], bool], + ) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def count(self) -> int: + return len(self._hooks) + + def __call__( + self, + will_reset: bool, + reason: Union[aqt.main.ResetReason, str], + context: Optional[Any], + ) -> bool: + for filter in self._hooks: + try: + will_reset = filter(will_reset, reason, context) + except: + # if the hook fails, remove it + self._hooks.remove(filter) + raise + return will_reset + + +main_window_should_require_reset = _MainWindowShouldRequireResetFilter() + + class _MediaSyncDidProgressHook: _hooks: List[Callable[["aqt.mediasync.LogEntryWithTime"], None]] = [] diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 60b09a6ad..73332af14 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -4,6 +4,7 @@ from __future__ import annotations +import enum import faulthandler import gc import os @@ -68,6 +69,19 @@ from aqt.utils import ( install_pylib_legacy() +class ResetReason(enum.Enum): + AddCardsAddNote = "addCardsAddNote" + EditCurrentInit = "editCurrentInit" + EditorBridgeCmd = "editorBridgeCmd" + BrowserSetDeck = "browserSetDeck" + BrowserAddTags = "browserAddTags" + BrowserSuspend = "browserSuspend" + BrowserReposition = "browserReposition" + BrowserReschedule = "browserReschedule" + BrowserFindReplace = "browserFindReplace" + BrowserTagDupes = "browserTagDupes" + + class ResetRequired: def __init__(self, mw: AnkiQt): self.mw = mw @@ -684,11 +698,13 @@ from the profile screen." self.maybeEnableUndo() self.moveToState(self.state) - def requireReset(self, modal=False): + def requireReset(self, modal=False, reason="unknown", context=None): "Signal queue needs to be rebuilt when edits are finished or by user." self.autosave() self.resetModal = modal - if self.interactiveState(): + if gui_hooks.main_window_should_require_reset( + self.interactiveState(), reason, context + ): self.moveToState("resetRequired") def interactiveState(self): diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index a9d5b1096..ca00e4578 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -440,6 +440,24 @@ hooks = [ is thus suitable for single-shot subscribers. """, ), + Hook( + name="main_window_should_require_reset", + args=[ + "will_reset: bool", + "reason: Union[aqt.main.ResetReason, str]", + "context: Optional[Any]", + ], + return_type="bool", + doc="""Executed before the main window will require a reset + + This hook can be used to change the behavior of the main window, + when other dialogs, like the AddCards or Browser, require a reset + from the main window. + If you decide to use this hook, make you sure you check the reason for the reset. + Some reasons require more attention than others, and skipping important ones might + put the main window into an invalid state (e.g. display a deleted note). + """, + ), Hook(name="backup_did_complete"), Hook( name="profile_did_open",