From e9dfb7a13d742acdd8b26e986e9bceebad4b629c Mon Sep 17 00:00:00 2001 From: Aristotelis <5459332+glutanimate@users.noreply.github.com> Date: Tue, 22 Apr 2025 13:22:40 +0200 Subject: [PATCH] Fix `AnkiWebPage` not being initialized for default web view kinds (e.g. in add-ons) (#3933) * add AnkiWebView subclasses for stats, empty cards and find dupes ui * update ui files to use subclassed webviews instead * remove superfluous calls to AnkiWebView.set_kind * Avoid set_kind() race condition in legacy stats webview Replacing the web view is a hacky workaround, but likely a reasonable compromise for a legacy view that we do not want to maintain a separate Qt form for. * Slightly refactor AnkiWebView subclass creation and tweak inline comment + Extend create_ankiwebview_subclass() with the ability to set any init time AnkiWebView argument + Introduce some nice-to-haves in terms of static type checking support and IDE autocompletion + Mark helper function as private to discourage add-on use * Drop `AnkiWebView.set_kind` completely There no longer is an Anki-internal use case for changing the web view kind after initializing a web view, and add-ons almost certainly do not have any use for it either. Given that setting the kind after web view construction can lead to known race conditions with `domDone` signals, we should remove this method to discourage uses like this in both Anki code and add-on consumers. There currenty only seems to be one add-on calling `set_kind()`, so this seem like a justifiable API change. --------- Co-authored-by: llama <100429699+iamllama@users.noreply.github.com> (cherry picked from commit 5b0f371791e2bfeb2a5b063a5b140af38fdd47d4) --- qt/aqt/browser/find_duplicates.py | 2 - qt/aqt/emptycards.py | 2 - qt/aqt/forms/emptycards.ui | 4 +- qt/aqt/forms/finddupes.ui | 4 +- qt/aqt/forms/stats.ui | 4 +- qt/aqt/stats.py | 7 ++-- qt/aqt/webview.py | 69 +++++++++++++++++++++++++------ 7 files changed, 66 insertions(+), 26 deletions(-) diff --git a/qt/aqt/browser/find_duplicates.py b/qt/aqt/browser/find_duplicates.py index 5ffb97ba5..d8454fec5 100644 --- a/qt/aqt/browser/find_duplicates.py +++ b/qt/aqt/browser/find_duplicates.py @@ -14,7 +14,6 @@ from anki.collection import SearchNode from anki.notes import NoteId from aqt.qt import * from aqt.qt import sip -from aqt.webview import AnkiWebViewKind from ..operations import QueryOp from ..operations.tag import add_tags_to_notes @@ -52,7 +51,6 @@ class FindDuplicatesDialog(QDialog): self._dupes: list[tuple[str, list[NoteId]]] = [] # links - form.webView.set_kind(AnkiWebViewKind.FIND_DUPLICATES) form.webView.set_bridge_command(self._on_duplicate_clicked, context=self) form.webView.stdHtml("", context=self) diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 78436239e..cf8bbd4ba 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -15,7 +15,6 @@ from anki.collection import EmptyCardsReport from aqt import gui_hooks from aqt.qt import QDialog, QDialogButtonBox, qconnect from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip, tr -from aqt.webview import AnkiWebViewKind def show_empty_cards(mw: aqt.main.AnkiQt) -> None: @@ -47,7 +46,6 @@ class EmptyCardsDialog(QDialog): self.setWindowTitle(tr.empty_cards_window_title()) disable_help_button(self) self.form.keep_notes.setText(tr.empty_cards_preserve_notes_checkbox()) - self.form.webview.set_kind(AnkiWebViewKind.EMPTY_CARDS) self.form.webview.set_bridge_command(self._on_note_link_clicked, self) gui_hooks.empty_cards_will_show(self) diff --git a/qt/aqt/forms/emptycards.ui b/qt/aqt/forms/emptycards.ui index 70049a47f..ad47dead3 100644 --- a/qt/aqt/forms/emptycards.ui +++ b/qt/aqt/forms/emptycards.ui @@ -30,7 +30,7 @@ 0 - + about:blank @@ -81,7 +81,7 @@ - AnkiWebView + EmptyCardsWebView QWidget
aqt/webview
1 diff --git a/qt/aqt/forms/finddupes.ui b/qt/aqt/forms/finddupes.ui index d47f8b635..9a7c44c06 100644 --- a/qt/aqt/forms/finddupes.ui +++ b/qt/aqt/forms/finddupes.ui @@ -73,7 +73,7 @@ 0 - + about:blank @@ -98,7 +98,7 @@ - AnkiWebView + FindDupesWebView QWidget
aqt/webview
1 diff --git a/qt/aqt/forms/stats.ui b/qt/aqt/forms/stats.ui index 0d0edaa53..838e1da5f 100644 --- a/qt/aqt/forms/stats.ui +++ b/qt/aqt/forms/stats.ui @@ -30,7 +30,7 @@ 0 - + about:blank @@ -146,7 +146,7 @@ - AnkiWebView + StatsWebView QWidget
aqt/webview
1 diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 3c9952155..56fb7ccfd 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -25,7 +25,7 @@ from aqt.utils import ( tooltip, tr, ) -from aqt.webview import AnkiWebViewKind +from aqt.webview import LegacyStatsWebView class NewDeckStats(QDialog): @@ -71,7 +71,6 @@ class NewDeckStats(QDialog): maybeHideClose(self.form.buttonBox) addCloseShortcut(self) gui_hooks.stats_dialog_will_show(self) - self.form.web.set_kind(AnkiWebViewKind.DECK_STATS) self.form.web.hide_while_preserving_layout() self.show() self.refresh() @@ -154,6 +153,9 @@ class DeckStats(QDialog): self.name = "deckStats" self.period = 0 self.form = aqt.forms.stats.Ui_Dialog() + # Hack: Switch out web views dynamically to avoid maintaining multiple + # Qt forms for different versions of the stats dialog. + self.form.web = LegacyStatsWebView(self.mw) self.oldPos = None self.wholeCollection = False self.setMinimumWidth(700) @@ -232,7 +234,6 @@ class DeckStats(QDialog): stats = self.mw.col.stats() stats.wholeCollection = self.wholeCollection self.report = stats.report(type=self.period) - self.form.web.set_kind(AnkiWebViewKind.LEGACY_DECK_STATS) self.form.web.stdHtml( f"{self.report}", js=["js/vendor/jquery.min.js", "js/vendor/plot.js"], diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 70bc5a25c..c6dc9db2d 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -10,7 +10,9 @@ import re import sys from collections.abc import Callable, Sequence from enum import Enum -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Type, cast + +from typing_extensions import TypedDict, Unpack import anki import anki.lang @@ -360,7 +362,9 @@ class AnkiWebView(QWebEngineView): kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT, ) -> None: QWebEngineView.__init__(self, parent=parent) - self.set_kind(kind) + self._kind = kind + self.set_title(kind.value) + self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self)) # reduce flicker self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS)) @@ -390,17 +394,6 @@ class AnkiWebView(QWebEngineView): """ ) - def set_kind(self, kind: AnkiWebViewKind) -> None: - self._kind = kind - self.set_title(kind.value) - # this is an ugly hack to avoid breakages caused by - # creating a default webview then immediately calling set_kind, which results - # in the creation of two pages, and the second fails as the domDone - # signal from the first one is received - if kind != AnkiWebViewKind.DEFAULT: - self.setPage(AnkiWebPage(self._onBridgeCmd, kind, self)) - self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS)) - def page(self) -> AnkiWebPage: return cast(AnkiWebPage, super().page()) @@ -965,3 +958,53 @@ html {{ {font} }} @deprecated(info="use theme_manager.qcolor() instead") def get_window_bg_color(self, night_mode: bool | None = None) -> QColor: return theme_manager.qcolor(colors.CANVAS) + + +# Pre-configured classes for use in Qt Designer +########################################################################## + + +class _AnkiWebViewKwargs(TypedDict, total=False): + parent: QWidget | None + title: str + kind: AnkiWebViewKind + + +def _create_ankiwebview_subclass( + name: str, + /, + **fixed_kwargs: Unpack[_AnkiWebViewKwargs], +) -> Type[AnkiWebView]: + + def __init__(self, *args: Any, **kwargs: _AnkiWebViewKwargs) -> None: + # user‑supplied kwargs override fixed kwargs + merged = cast(_AnkiWebViewKwargs, {**fixed_kwargs, **kwargs}) + AnkiWebView.__init__(self, *args, **merged) + + __init__.__qualname__ = f"{name}.__init__" + if fixed_kwargs: + __init__.__doc__ = ( + f"Auto‑generated wrapper that pre‑sets " + f"{', '.join(f'{k}={v!r}' for k, v in fixed_kwargs.items())}." + ) + + cls: Type[AnkiWebView] = type(name, (AnkiWebView,), {"__init__": __init__}) + + return cls + + +# These subclasses are used in Qt Designer UI files to allow for configuring +# web views at initialization time (custom widgets can otherwise only be +# initialized with the default constructor) +StatsWebView = _create_ankiwebview_subclass( + "StatsWebView", kind=AnkiWebViewKind.DECK_STATS +) +LegacyStatsWebView = _create_ankiwebview_subclass( + "LegacyStatsWebView", kind=AnkiWebViewKind.LEGACY_DECK_STATS +) +EmptyCardsWebView = _create_ankiwebview_subclass( + "EmptyCardsWebView", kind=AnkiWebViewKind.EMPTY_CARDS +) +FindDupesWebView = _create_ankiwebview_subclass( + "FindDupesWebView", kind=AnkiWebViewKind.FIND_DUPLICATES +)