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 5b0f371791)
This commit is contained in:
Aristotelis 2025-04-22 13:22:40 +02:00 committed by Damien Elmes
parent fa1d6eae84
commit e9dfb7a13d
7 changed files with 66 additions and 26 deletions

View file

@ -14,7 +14,6 @@ from anki.collection import SearchNode
from anki.notes import NoteId from anki.notes import NoteId
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip from aqt.qt import sip
from aqt.webview import AnkiWebViewKind
from ..operations import QueryOp from ..operations import QueryOp
from ..operations.tag import add_tags_to_notes from ..operations.tag import add_tags_to_notes
@ -52,7 +51,6 @@ class FindDuplicatesDialog(QDialog):
self._dupes: list[tuple[str, list[NoteId]]] = [] self._dupes: list[tuple[str, list[NoteId]]] = []
# links # links
form.webView.set_kind(AnkiWebViewKind.FIND_DUPLICATES)
form.webView.set_bridge_command(self._on_duplicate_clicked, context=self) form.webView.set_bridge_command(self._on_duplicate_clicked, context=self)
form.webView.stdHtml("", context=self) form.webView.stdHtml("", context=self)

View file

@ -15,7 +15,6 @@ from anki.collection import EmptyCardsReport
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import QDialog, QDialogButtonBox, qconnect from aqt.qt import QDialog, QDialogButtonBox, qconnect
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip, tr 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: def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
@ -47,7 +46,6 @@ class EmptyCardsDialog(QDialog):
self.setWindowTitle(tr.empty_cards_window_title()) self.setWindowTitle(tr.empty_cards_window_title())
disable_help_button(self) disable_help_button(self)
self.form.keep_notes.setText(tr.empty_cards_preserve_notes_checkbox()) 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) self.form.webview.set_bridge_command(self._on_note_link_clicked, self)
gui_hooks.empty_cards_will_show(self) gui_hooks.empty_cards_will_show(self)

View file

@ -30,7 +30,7 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="AnkiWebView" name="webview" native="true"> <widget class="EmptyCardsWebView" name="webview" native="true">
<property name="url" stdset="0"> <property name="url" stdset="0">
<url> <url>
<string notr="true">about:blank</string> <string notr="true">about:blank</string>
@ -81,7 +81,7 @@
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
<class>AnkiWebView</class> <class>EmptyCardsWebView</class>
<extends>QWidget</extends> <extends>QWidget</extends>
<header location="global">aqt/webview</header> <header location="global">aqt/webview</header>
<container>1</container> <container>1</container>

View file

@ -73,7 +73,7 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="AnkiWebView" name="webView" native="true"> <widget class="FindDupesWebView" name="webView" native="true">
<property name="url" stdset="0"> <property name="url" stdset="0">
<url> <url>
<string notr="true">about:blank</string> <string notr="true">about:blank</string>
@ -98,7 +98,7 @@
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
<class>AnkiWebView</class> <class>FindDupesWebView</class>
<extends>QWidget</extends> <extends>QWidget</extends>
<header location="global">aqt/webview</header> <header location="global">aqt/webview</header>
<container>1</container> <container>1</container>

View file

@ -30,7 +30,7 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="AnkiWebView" name="web" native="true"> <widget class="StatsWebView" name="web" native="true">
<property name="url" stdset="0"> <property name="url" stdset="0">
<url> <url>
<string notr="true">about:blank</string> <string notr="true">about:blank</string>
@ -146,7 +146,7 @@
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
<class>AnkiWebView</class> <class>StatsWebView</class>
<extends>QWidget</extends> <extends>QWidget</extends>
<header location="global">aqt/webview</header> <header location="global">aqt/webview</header>
<container>1</container> <container>1</container>

View file

@ -25,7 +25,7 @@ from aqt.utils import (
tooltip, tooltip,
tr, tr,
) )
from aqt.webview import AnkiWebViewKind from aqt.webview import LegacyStatsWebView
class NewDeckStats(QDialog): class NewDeckStats(QDialog):
@ -71,7 +71,6 @@ class NewDeckStats(QDialog):
maybeHideClose(self.form.buttonBox) maybeHideClose(self.form.buttonBox)
addCloseShortcut(self) addCloseShortcut(self)
gui_hooks.stats_dialog_will_show(self) gui_hooks.stats_dialog_will_show(self)
self.form.web.set_kind(AnkiWebViewKind.DECK_STATS)
self.form.web.hide_while_preserving_layout() self.form.web.hide_while_preserving_layout()
self.show() self.show()
self.refresh() self.refresh()
@ -154,6 +153,9 @@ class DeckStats(QDialog):
self.name = "deckStats" self.name = "deckStats"
self.period = 0 self.period = 0
self.form = aqt.forms.stats.Ui_Dialog() 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.oldPos = None
self.wholeCollection = False self.wholeCollection = False
self.setMinimumWidth(700) self.setMinimumWidth(700)
@ -232,7 +234,6 @@ class DeckStats(QDialog):
stats = self.mw.col.stats() stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection stats.wholeCollection = self.wholeCollection
self.report = stats.report(type=self.period) self.report = stats.report(type=self.period)
self.form.web.set_kind(AnkiWebViewKind.LEGACY_DECK_STATS)
self.form.web.stdHtml( self.form.web.stdHtml(
f"<html><body>{self.report}</body></html>", f"<html><body>{self.report}</body></html>",
js=["js/vendor/jquery.min.js", "js/vendor/plot.js"], js=["js/vendor/jquery.min.js", "js/vendor/plot.js"],

View file

@ -10,7 +10,9 @@ import re
import sys import sys
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from enum import Enum 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
import anki.lang import anki.lang
@ -360,7 +362,9 @@ class AnkiWebView(QWebEngineView):
kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT, kind: AnkiWebViewKind = AnkiWebViewKind.DEFAULT,
) -> None: ) -> None:
QWebEngineView.__init__(self, parent=parent) 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 # reduce flicker
self.page().setBackgroundColor(theme_manager.qcolor(colors.CANVAS)) 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: def page(self) -> AnkiWebPage:
return cast(AnkiWebPage, super().page()) return cast(AnkiWebPage, super().page())
@ -965,3 +958,53 @@ html {{ {font} }}
@deprecated(info="use theme_manager.qcolor() instead") @deprecated(info="use theme_manager.qcolor() instead")
def get_window_bg_color(self, night_mode: bool | None = None) -> QColor: def get_window_bg_color(self, night_mode: bool | None = None) -> QColor:
return theme_manager.qcolor(colors.CANVAS) 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:
# usersupplied 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"Autogenerated wrapper that presets "
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
)