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
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
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
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
+)