Merge pull request #445 from glutanimate/new-html-view-hooks-2

Allow add-on authors to easily inject their own content into Anki's web views – take 3
This commit is contained in:
Damien Elmes 2020-02-16 08:32:34 +10:00 committed by GitHub
commit 990a6c394b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 288 additions and 77 deletions

View File

@ -219,5 +219,5 @@ suggestions, bug reports and donations."
abt.label.setMinimumWidth(800) abt.label.setMinimumWidth(800)
abt.label.setMinimumHeight(600) abt.label.setMinimumHeight(600)
dialog.show() dialog.show()
abt.label.stdHtml(abouttext, js=" ") abt.label.stdHtml(abouttext, js=[])
return dialog return dialog

View File

@ -1384,27 +1384,20 @@ by clicking on one on the left."""
info, cs = self._cardInfoData() info, cs = self._cardInfoData()
reps = self._revlogData(cs) reps = self._revlogData(cs)
class CardInfoDialog(QDialog): card_info_dialog = CardInfoDialog(self)
silentlyClose = True
def reject(self):
saveGeom(self, "revlog")
return QDialog.reject(self)
d = CardInfoDialog(self)
l = QVBoxLayout() l = QVBoxLayout()
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
w = AnkiWebView() w = AnkiWebView(title="browser card info")
l.addWidget(w) l.addWidget(w)
w.stdHtml(info + "<p>" + reps) w.stdHtml(info + "<p>" + reps, context=card_info_dialog)
bb = QDialogButtonBox(QDialogButtonBox.Close) bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(bb) l.addWidget(bb)
bb.rejected.connect(d.reject) bb.rejected.connect(card_info_dialog.reject)
d.setLayout(l) card_info_dialog.setLayout(l)
d.setWindowModality(Qt.WindowModal) card_info_dialog.setWindowModality(Qt.WindowModal)
d.resize(500, 400) card_info_dialog.resize(500, 400)
restoreGeom(d, "revlog") restoreGeom(card_info_dialog, "revlog", CardInfoDialog)
d.show() card_info_dialog.show()
def _cardInfoData(self): def _cardInfoData(self):
from anki.stats import CardStats from anki.stats import CardStats
@ -1563,7 +1556,7 @@ where id in %s"""
self._previewWindow.silentlyClose = True self._previewWindow.silentlyClose = True
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0) vbox.setContentsMargins(0, 0, 0, 0)
self._previewWeb = AnkiWebView() self._previewWeb = AnkiWebView(title="previewer")
vbox.addWidget(self._previewWeb) vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox() bbox = QDialogButtonBox()
@ -1658,12 +1651,15 @@ where id in %s"""
"mathjax/MathJax.js", "mathjax/MathJax.js",
"reviewer.js", "reviewer.js",
] ]
web_context = PreviewDialog(dialog=self._previewWindow, browser=self)
self._previewWeb.stdHtml( self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc self.mw.reviewer.revHtml(),
css=["reviewer.css"],
js=jsinc,
context=web_context,
) )
self._previewWeb.set_bridge_command( self._previewWeb.set_bridge_command(
self._on_preview_bridge_cmd, self._on_preview_bridge_cmd, web_context,
PreviewDialog(dialog=self._previewWindow, browser=self),
) )
def _on_preview_bridge_cmd(self, cmd: str) -> Any: def _on_preview_bridge_cmd(self, cmd: str) -> Any:
@ -2161,10 +2157,10 @@ update cards set usn=?, mod=?, did=? where id in """
frm.fields.addItems(fields) frm.fields.addItems(fields)
self._dupesButton = None self._dupesButton = None
# links # links
frm.webView.set_bridge_command( frm.webView.title = "find duplicates"
self.dupeLinkClicked, FindDupesDialog(dialog=d, browser=self) web_context = FindDupesDialog(dialog=d, browser=self)
) frm.webView.set_bridge_command(self.dupeLinkClicked, web_context)
frm.webView.stdHtml("") frm.webView.stdHtml("", context=web_context)
def onFin(code): def onFin(code):
saveGeom(d, "findDupes") saveGeom(d, "findDupes")
@ -2173,13 +2169,15 @@ update cards set usn=?, mod=?, did=? where id in """
def onClick(): def onClick():
field = fields[frm.fields.currentIndex()] field = fields[frm.fields.currentIndex()]
self.duplicatesReport(frm.webView, field, frm.search.text(), frm) self.duplicatesReport(
frm.webView, field, frm.search.text(), frm, web_context
)
search = frm.buttonBox.addButton(_("Search"), QDialogButtonBox.ActionRole) search = frm.buttonBox.addButton(_("Search"), QDialogButtonBox.ActionRole)
search.clicked.connect(onClick) search.clicked.connect(onClick)
d.show() d.show()
def duplicatesReport(self, web, fname, search, frm): def duplicatesReport(self, web, fname, search, frm, web_context):
self.mw.progress.start() self.mw.progress.start()
res = self.mw.col.findDupes(fname, search) res = self.mw.col.findDupes(fname, search)
if not self._dupesButton: if not self._dupesButton:
@ -2204,7 +2202,7 @@ update cards set usn=?, mod=?, did=? where id in """
) )
) )
t += "</ol>" t += "</ol>"
web.stdHtml(t) web.stdHtml(t, context=web_context)
self.mw.progress.finish() self.mw.progress.finish()
def _onTagDupes(self, res): def _onTagDupes(self, res):
@ -2478,3 +2476,19 @@ Are you sure you want to continue?"""
def onHelp(self): def onHelp(self):
openHelp("browsermisc") openHelp("browsermisc")
# Card Info Dialog
######################################################################
class CardInfoDialog(QDialog):
silentlyClose = True
def __init__(self, browser: Browser, *args, **kwargs):
super().__init__(browser, *args, **kwargs)
self.browser = browser
def reject(self):
saveGeom(self, "revlog")
return QDialog.reject(self)

View File

@ -203,9 +203,9 @@ class CardLayout(QDialog):
def setupWebviews(self): def setupWebviews(self):
pform = self.pform pform = self.pform
pform.frontWeb = AnkiWebView() pform.frontWeb = AnkiWebView(title="card layout front")
pform.frontPrevBox.addWidget(pform.frontWeb) pform.frontPrevBox.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView() pform.backWeb = AnkiWebView(title="card layout back")
pform.backPrevBox.addWidget(pform.backWeb) pform.backPrevBox.addWidget(pform.backWeb)
jsinc = [ jsinc = [
"jquery.js", "jquery.js",
@ -215,10 +215,10 @@ class CardLayout(QDialog):
"reviewer.js", "reviewer.js",
] ]
pform.frontWeb.stdHtml( pform.frontWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
) )
pform.backWeb.stdHtml( pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
) )
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self) pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self)
pform.backWeb.set_bridge_command(self._on_bridge_cmd, self) pform.backWeb.set_bridge_command(self._on_bridge_cmd, self)

View File

@ -109,6 +109,7 @@ class DeckBrowser:
self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()), self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()),
css=["deckbrowser.css"], css=["deckbrowser.css"],
js=["jquery.js", "jquery-ui.js", "deckbrowser.js"], js=["jquery.js", "jquery-ui.js", "deckbrowser.js"],
context=self,
) )
self.web.key = "deckBrowser" self.web.key = "deckBrowser"
self._drawButtons() self._drawButtons()
@ -340,9 +341,10 @@ where id > ?""",
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple( <button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(
b b
) )
self.bottom.draw(buf) self.bottom.draw(
self.bottom.web.set_bridge_command( buf=buf,
self._linkHandler, DeckBrowserBottomBar(self) link_handler=self._linkHandler,
web_context=DeckBrowserBottomBar(self),
) )
def _onShared(self): def _onShared(self):

View File

@ -95,7 +95,6 @@ class Editor:
def setupWeb(self) -> None: def setupWeb(self) -> None:
self.web = EditorWebView(self.widget, self) self.web = EditorWebView(self.widget, self)
self.web.title = "editor"
self.web.allowDrops = True self.web.allowDrops = True
self.web.set_bridge_command(self.onBridgeCmd, self) self.web.set_bridge_command(self.onBridgeCmd, self)
self.outerLayout.addWidget(self.web, 1) self.outerLayout.addWidget(self.web, 1)
@ -167,6 +166,7 @@ class Editor:
_html % (bgcol, bgcol, topbuts, _("Show Duplicates")), _html % (bgcol, bgcol, topbuts, _("Show Duplicates")),
css=["editor.css"], css=["editor.css"],
js=["jquery.js", "editor.js"], js=["jquery.js", "editor.js"],
context=self,
) )
# Top buttons # Top buttons
@ -937,7 +937,7 @@ to a cloze type first, via Edit>Change Note Type."""
class EditorWebView(AnkiWebView): class EditorWebView(AnkiWebView):
def __init__(self, parent, editor): def __init__(self, parent, editor):
AnkiWebView.__init__(self) AnkiWebView.__init__(self, title="editor")
self.editor = editor self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"] self.strip = self.editor.mw.pm.profile["stripHTML"]
self.setAcceptDrops(True) self.setAcceptDrops(True)

View File

@ -7,7 +7,7 @@ See pylib/anki/hooks.py
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Dict, List, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
import anki import anki
import aqt import aqt
@ -1129,6 +1129,67 @@ class _WebviewDidReceiveJsMessageFilter:
webview_did_receive_js_message = _WebviewDidReceiveJsMessageFilter() webview_did_receive_js_message = _WebviewDidReceiveJsMessageFilter()
class _WebviewWillSetContentHook:
"""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
def on_webview_will_set_content(web_content: WebContent, context):
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
"""
_hooks: List[Callable[["aqt.webview.WebContent", Optional[Any]], None]] = []
def append(
self, cb: Callable[["aqt.webview.WebContent", Optional[Any]], None]
) -> None:
"""(web_content: aqt.webview.WebContent, context: Optional[Any])"""
self._hooks.append(cb)
def remove(
self, cb: Callable[["aqt.webview.WebContent", Optional[Any]], None]
) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(
self, web_content: aqt.webview.WebContent, context: Optional[Any]
) -> None:
for hook in self._hooks:
try:
hook(web_content, context)
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
webview_will_set_content = _WebviewWillSetContentHook()
class _WebviewWillShowContextMenuHook: class _WebviewWillShowContextMenuHook:
_hooks: List[Callable[["aqt.webview.AnkiWebView", QMenu], None]] = [] _hooks: List[Callable[["aqt.webview.AnkiWebView", QMenu], None]] = []

View File

@ -663,9 +663,8 @@ from the profile screen."
if self.resetModal: if self.resetModal:
# we don't have to change the webview, as we have a covering window # we don't have to change the webview, as we have a covering window
return return
self.web.set_bridge_command( web_context = ResetRequired(self)
lambda url: self.delayedMaybeReset(), ResetRequired(self) self.web.set_bridge_command(lambda url: self.delayedMaybeReset(), web_context)
)
i = _("Waiting for editing to finish.") i = _("Waiting for editing to finish.")
b = self.button("refresh", _("Resume Now"), id="resume") b = self.button("refresh", _("Resume Now"), id="resume")
self.web.stdHtml( self.web.stdHtml(
@ -676,7 +675,8 @@ from the profile screen."
%s</div></div></center> %s</div></div></center>
<script>$('#resume').focus()</script> <script>$('#resume').focus()</script>
""" """
% (i, b) % (i, b),
context=web_context,
) )
self.bottomWeb.hide() self.bottomWeb.hide()
self.web.setFocus() self.web.setFocus()
@ -717,19 +717,16 @@ title="%s" %s>%s</button>""" % (
self.form = aqt.forms.main.Ui_MainWindow() self.form = aqt.forms.main.Ui_MainWindow()
self.form.setupUi(self) self.form.setupUi(self)
# toolbar # toolbar
tweb = self.toolbarWeb = aqt.webview.AnkiWebView() tweb = self.toolbarWeb = aqt.webview.AnkiWebView(title="top toolbar")
tweb.title = "top toolbar"
tweb.setFocusPolicy(Qt.WheelFocus) tweb.setFocusPolicy(Qt.WheelFocus)
self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar = aqt.toolbar.Toolbar(self, tweb)
self.toolbar.draw() self.toolbar.draw()
# main area # main area
self.web = aqt.webview.AnkiWebView() self.web = aqt.webview.AnkiWebView(title="main webview")
self.web.title = "main webview"
self.web.setFocusPolicy(Qt.WheelFocus) self.web.setFocusPolicy(Qt.WheelFocus)
self.web.setMinimumWidth(400) self.web.setMinimumWidth(400)
# bottom area # bottom area
sweb = self.bottomWeb = aqt.webview.AnkiWebView() sweb = self.bottomWeb = aqt.webview.AnkiWebView(title="bottom toolbar")
sweb.title = "bottom toolbar"
sweb.setFocusPolicy(Qt.WheelFocus) sweb.setFocusPolicy(Qt.WheelFocus)
# add in a layout # add in a layout
self.mainLayout = QVBoxLayout() self.mainLayout = QVBoxLayout()

View File

@ -151,6 +151,7 @@ class Overview:
), ),
css=["overview.css"], css=["overview.css"],
js=["jquery.js", "overview.js"], js=["jquery.js", "overview.js"],
context=self,
) )
def _desc(self, deck): def _desc(self, deck):
@ -243,8 +244,9 @@ to their original deck."""
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple( <button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(
b b
) )
self.bottom.draw(buf) self.bottom.draw(
self.bottom.web.set_bridge_command(self._linkHandler, OverviewBottomBar(self)) buf=buf, link_handler=self._linkHandler, web_context=OverviewBottomBar(self)
)
# Studying more # Studying more
###################################################################### ######################################################################

View File

@ -153,6 +153,7 @@ class Reviewer:
"mathjax/MathJax.js", "mathjax/MathJax.js",
"reviewer.js", "reviewer.js",
], ],
context=self,
) )
# show answer / ease buttons # show answer / ease buttons
self.bottom.web.show() self.bottom.web.show()
@ -160,6 +161,7 @@ class Reviewer:
self._bottomHTML(), self._bottomHTML(),
css=["toolbar-bottom.css", "reviewer-bottom.css"], css=["toolbar-bottom.css", "reviewer-bottom.css"],
js=["jquery.js", "reviewer-bottom.js"], js=["jquery.js", "reviewer-bottom.js"],
context=ReviewerBottomBar(self),
) )
# Showing the question # Showing the question

View File

@ -95,7 +95,10 @@ 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.title = "deck stats"
self.form.web.stdHtml( self.form.web.stdHtml(
"<html><body>" + self.report + "</body></html>", js=["jquery.js", "plot.js"] "<html><body>" + self.report + "</body></html>",
js=["jquery.js", "plot.js"],
context=self,
) )
self.mw.progress.finish() self.mw.progress.finish()

View File

@ -4,6 +4,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional
import aqt import aqt
from anki.lang import _ from anki.lang import _
from aqt.qt import * from aqt.qt import *
@ -37,9 +39,18 @@ class Toolbar:
self.web.setFixedHeight(30) self.web.setFixedHeight(30)
self.web.requiresCol = False self.web.requiresCol = False
def draw(self): def draw(
self.web.set_bridge_command(self._linkHandler, TopToolbar(self)) self,
self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"]) buf: str = "",
web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None,
):
web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context)
self.web.stdHtml(
self._body % self._centerLinks(), css=["toolbar.css"], context=web_context,
)
self.web.adjustHeightToFit() self.web.adjustHeightToFit()
# Available links # Available links
@ -122,10 +133,19 @@ class BottomBar(Toolbar):
%s</td></tr></table></center> %s</td></tr></table></center>
""" """
def draw(self, buf): def draw(
self,
buf: str = "",
web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None,
):
# note: some screens may override this # note: some screens may override this
self.web.set_bridge_command(self._linkHandler, BottomToolbar(self)) web_context = web_context or BottomToolbar(self)
link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context)
self.web.stdHtml( self.web.stdHtml(
self._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"] self._centerBody % buf,
css=["toolbar.css", "toolbar-bottom.css"],
context=web_context,
) )
self.web.adjustHeightToFit() self.web.adjustHeightToFit()

View File

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import dataclasses
import json import json
import math import math
import sys import sys
@ -96,14 +97,76 @@ class AnkiWebPage(QWebEnginePage): # type: ignore
return self._onBridgeCmd(str) return self._onBridgeCmd(str)
# Add-ons
##########################################################################
@dataclasses.dataclass
class WebContent:
"""Stores all dynamically modified content that a particular web view
will be populated with.
Attributes:
body {str} -- HTML body
head {str} -- HTML head
css {List[str]} -- List of media server subpaths,
each pointing to a CSS file
js {List[str]} -- List of media server subpaths,
each pointing to a JS file
Important Notes:
- When modifying the attributes specified above, please make sure your
changes only perform the minimum requried edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_webview_will_set_content(web_content: WebContent, context):
web_content.body += "<my_html>"
web_content.head += "<my_head>"
- The paths specified in `css` and `js` need to be accessible by Anki's
media server. All list members without a specified subpath are assumed
to be located under `/_anki`, which is the media server subpath used
for all web assets shipped with Anki.
Add-ons may expose their own web assets by utilizing
aqt.addons.AddonManager.setWebExports(). Web exports registered
in this manner may then be accessed under the `/_addons` subpath.
E.g., to allow access to a `my-addon.js` and `my-addon.css` residing
in a "web" subfolder in your add-on package, first register the
corresponding web export:
> from aqt import mw
> mw.addonManager.setWebExports(__name__, r"web/.*(css|js)")
Then append the subpaths to the corresponding web_content fields
within a function subscribing to gui_hooks.webview_will_set_content:
def on_webview_will_set_content(web_content: WebContent, context):
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
"""
body: str = ""
head: str = ""
css: List[str] = dataclasses.field(default_factory=lambda: [])
js: List[str] = dataclasses.field(default_factory=lambda: [])
# Main web view # Main web view
########################################################################## ##########################################################################
class AnkiWebView(QWebEngineView): # type: ignore class AnkiWebView(QWebEngineView): # type: ignore
def __init__(self, parent: Optional[QWidget] = None) -> None: def __init__(
self, parent: Optional[QWidget] = None, title: str = "default"
) -> None:
QWebEngineView.__init__(self, parent=parent) # type: ignore QWebEngineView.__init__(self, parent=parent) # type: ignore
self.title = "default" self.title = title
self._page = AnkiWebPage(self._onBridgeCmd) self._page = AnkiWebPage(self._onBridgeCmd)
self._page.setBackgroundColor(self._getWindowColor()) # reduce flicker self._page.setBackgroundColor(self._getWindowColor()) # reduce flicker
@ -254,11 +317,23 @@ class AnkiWebView(QWebEngineView): # type: ignore
return QColor("#ececec") return QColor("#ececec")
return self.style().standardPalette().color(QPalette.Window) return self.style().standardPalette().color(QPalette.Window)
def stdHtml(self, body, css=None, js=None, head=""): def stdHtml(
if css is None: self,
css = [] body: str,
if js is None: css: Optional[List[str]] = None,
js = ["jquery.js"] js: Optional[List[str]] = None,
head: str = "",
context: Optional[Any] = None,
):
web_content = WebContent(
body=body,
head=head,
js=["webview.js"] + (["jquery.js"] if js is None else js),
css=["webview.css"] + ([] if css is None else css),
)
gui_hooks.webview_will_set_content(web_content, context)
palette = self.style().standardPalette() palette = self.style().standardPalette()
color_hl = palette.color(QPalette.Highlight).name() color_hl = palette.color(QPalette.Highlight).name()
@ -299,16 +374,12 @@ div[contenteditable="true"]:focus {
"color_hl_txt": color_hl_txt, "color_hl_txt": color_hl_txt,
} }
csstxt = "\n".join( csstxt = "\n".join(self.bundledCSS(fname) for fname in web_content.css)
[self.bundledCSS("webview.css")] + [self.bundledCSS(fname) for fname in css] jstxt = "\n".join(self.bundledScript(fname) for fname in web_content.js)
)
jstxt = "\n".join(
[self.bundledScript("webview.js")]
+ [self.bundledScript(fname) for fname in js]
)
from aqt import mw from aqt import mw
head = mw.baseHTML() + head + csstxt + jstxt head = mw.baseHTML() + web_content.head + csstxt + jstxt
body_class = theme_manager.body_class() body_class = theme_manager.body_class()
@ -334,20 +405,25 @@ body {{ zoom: {}; background: {}; {} }}
widgetspec, widgetspec,
head, head,
body_class, body_class,
body, web_content.body,
) )
# print(html) # print(html)
self.setHtml(html) self.setHtml(html)
def webBundlePath(self, path): def webBundlePath(self, path: str) -> str:
from aqt import mw from aqt import mw
return "http://127.0.0.1:%d/_anki/%s" % (mw.mediaServer.getPort(), path) if path.startswith("/"):
subpath = ""
else:
subpath = "/_anki/"
def bundledScript(self, fname): return f"http://127.0.0.1:{mw.mediaServer.getPort()}{subpath}{path}"
def bundledScript(self, fname: str) -> str:
return '<script src="%s"></script>' % self.webBundlePath(fname) return '<script src="%s"></script>' % self.webBundlePath(fname)
def bundledCSS(self, fname): def bundledCSS(self, fname: str) -> str:
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath( return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(
fname fname
) )

View File

@ -163,6 +163,40 @@ hooks = [
return handled return handled
""", """,
), ),
Hook(
name="webview_will_set_content",
args=["web_content: aqt.webview.WebContent", "context: Optional[Any]",],
doc="""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
def on_webview_will_set_content(web_content: WebContent, context):
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
""",
),
Hook( Hook(
name="webview_will_show_context_menu", name="webview_will_show_context_menu",
args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"], args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"],