Add webview_will_set_content hook & update supporting code accordingly

This commit is contained in:
Glutanimate 2020-02-12 22:00:13 +01:00
parent df2a7b06ef
commit bbd667b0ff
12 changed files with 243 additions and 70 deletions

View File

@ -1650,18 +1650,21 @@ where id in %s"""
def _setupPreviewWebview(self):
jsinc = [
"jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js",
"_anki/jquery.js",
"_anki/browsersel.js",
"_anki/mathjax/conf.js",
"_anki/mathjax/MathJax.js",
"_anki/reviewer.js",
]
web_context = PreviewDialog(dialog=self._previewWindow, browser=self)
self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(),
css=["_anki/reviewer.css"],
js=jsinc,
context=web_context,
)
self._previewWeb.set_bridge_command(
self._on_preview_bridge_cmd,
PreviewDialog(dialog=self._previewWindow, browser=self),
self._on_preview_bridge_cmd, web_context,
)
def _on_preview_bridge_cmd(self, cmd: str) -> Any:
@ -2159,10 +2162,9 @@ update cards set usn=?, mod=?, did=? where id in """
self._dupesButton = None
# links
frm.webView.title = "find duplicates"
frm.webView.set_bridge_command(
self.dupeLinkClicked, FindDupesDialog(dialog=d, browser=self)
)
frm.webView.stdHtml("")
web_context = FindDupesDialog(dialog=d, browser=self)
frm.webView.set_bridge_command(self.dupeLinkClicked, web_context)
frm.webView.stdHtml("", context=web_context)
def onFin(code):
saveGeom(d, "findDupes")
@ -2171,13 +2173,15 @@ update cards set usn=?, mod=?, did=? where id in """
def onClick():
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.clicked.connect(onClick)
d.show()
def duplicatesReport(self, web, fname, search, frm):
def duplicatesReport(self, web, fname, search, frm, web_context):
self.mw.progress.start()
res = self.mw.col.findDupes(fname, search)
if not self._dupesButton:
@ -2202,7 +2206,7 @@ update cards set usn=?, mod=?, did=? where id in """
)
)
t += "</ol>"
web.stdHtml(t)
web.stdHtml(t, context=web_context)
self.mw.progress.finish()
def _onTagDupes(self, res):

View File

@ -208,17 +208,17 @@ class CardLayout(QDialog):
pform.backWeb = AnkiWebView(title="card layout back")
pform.backPrevBox.addWidget(pform.backWeb)
jsinc = [
"jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js",
"_anki/jquery.js",
"_anki/browsersel.js",
"_anki/mathjax/conf.js",
"_anki/mathjax/MathJax.js",
"_anki/reviewer.js",
]
pform.frontWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(), css=["_anki/reviewer.css"], js=jsinc, context=self
)
pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(), css=["_anki/reviewer.css"], js=jsinc, context=self
)
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self)
pform.backWeb.set_bridge_command(self._on_bridge_cmd, self)

View File

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

View File

@ -164,8 +164,9 @@ class Editor:
# then load page
self.web.stdHtml(
_html % (bgcol, bgcol, topbuts, _("Show Duplicates")),
css=["editor.css"],
js=["jquery.js", "editor.js"],
css=["_anki/editor.css"],
js=["_anki/jquery.js", "_anki/editor.js"],
context=self,
)
# Top buttons

View File

@ -1129,6 +1129,56 @@ class _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:
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
web_content.js.append("my_addon.js")
web_content.css.append("my_addon.css")
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:
_hooks: List[Callable[["aqt.webview.AnkiWebView", QMenu], None]] = []

View File

@ -663,9 +663,8 @@ from the profile screen."
if self.resetModal:
# we don't have to change the webview, as we have a covering window
return
self.web.set_bridge_command(
lambda url: self.delayedMaybeReset(), ResetRequired(self)
)
web_context = ResetRequired(self)
self.web.set_bridge_command(lambda url: self.delayedMaybeReset(), web_context)
i = _("Waiting for editing to finish.")
b = self.button("refresh", _("Resume Now"), id="resume")
self.web.stdHtml(
@ -676,7 +675,8 @@ from the profile screen."
%s</div></div></center>
<script>$('#resume').focus()</script>
"""
% (i, b)
% (i, b),
context=web_context,
)
self.bottomWeb.hide()
self.web.setFocus()

View File

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

View File

@ -145,21 +145,23 @@ class Reviewer:
# main window
self.web.stdHtml(
self.revHtml(),
css=["reviewer.css"],
css=["_anki/reviewer.css"],
js=[
"jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js",
"_anki/jquery.js",
"_anki/browsersel.js",
"_anki/mathjax/conf.js",
"_anki/mathjax/MathJax.js",
"_anki/reviewer.js",
],
context=self,
)
# show answer / ease buttons
self.bottom.web.show()
self.bottom.web.stdHtml(
self._bottomHTML(),
css=["toolbar-bottom.css", "reviewer-bottom.css"],
js=["jquery.js", "reviewer-bottom.js"],
css=["_anki/toolbar-bottom.css", "_anki/reviewer-bottom.css"],
js=["_anki/jquery.js", "_anki/reviewer-bottom.js"],
context=ReviewerBottomBar(self),
)
# Showing the question

View File

@ -97,6 +97,8 @@ class DeckStats(QDialog):
self.report = stats.report(type=self.period)
self.form.web.title = "deck stats"
self.form.web.stdHtml(
"<html><body>" + self.report + "</body></html>", js=["jquery.js", "plot.js"]
"<html><body>" + self.report + "</body></html>",
js=["_anki/jquery.js", "_anki/plot.js"],
context=self,
)
self.mw.progress.finish()

View File

@ -4,6 +4,8 @@
from __future__ import annotations
from typing import Optional, Any
import aqt
from anki.lang import _
from aqt.qt import *
@ -37,9 +39,18 @@ class Toolbar:
self.web.setFixedHeight(30)
self.web.requiresCol = False
def draw(self):
self.web.set_bridge_command(self._linkHandler, TopToolbar(self))
self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"])
def draw(
self,
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=["_anki/toolbar.css"], context=web_context
)
self.web.adjustHeightToFit()
# Available links
@ -122,10 +133,19 @@ class BottomBar(Toolbar):
%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
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._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"]
self._centerBody % buf,
css=["_anki/toolbar.css", "_anki/toolbar-bottom.css"],
context=web_context,
)
self.web.adjustHeightToFit()

View File

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import dataclasses
import json
import math
import sys
@ -96,6 +97,64 @@ class AnkiWebPage(QWebEnginePage): # type: ignore
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>"
web_content.css.append("my_addon.css")
web_content.js.append("my_addon.js")
- The paths specified in `css` and `js` need to be accessible by Anki's media
server. Web components shipping with Anki are located under the `_anki`
subpath.
Add-ons may expose their own web components 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 an `addon.js` and `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/addon.css")
web_content.js.append(
f"_addons/{addon_package}/web/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
##########################################################################
@ -256,11 +315,23 @@ class AnkiWebView(QWebEngineView): # type: ignore
return QColor("#ececec")
return self.style().standardPalette().color(QPalette.Window)
def stdHtml(self, body, css=None, js=None, head=""):
if css is None:
css = []
if js is None:
js = ["jquery.js"]
def stdHtml(
self,
body: str,
css: Optional[List[str]] = None,
js: Optional[List[str]] = None,
head: str = "",
context: Optional[Any] = None,
):
web_content = WebContent(
body=body,
head=head,
js=["_anki/webview.js"] + (["_anki/jquery.js"] if js is None else js),
css=["_anki/webview.css"] + ([] if css is None else css),
)
gui_hooks.webview_will_set_content(web_content, context)
palette = self.style().standardPalette()
color_hl = palette.color(QPalette.Highlight).name()
@ -301,16 +372,12 @@ div[contenteditable="true"]:focus {
"color_hl_txt": color_hl_txt,
}
csstxt = "\n".join(
[self.bundledCSS("webview.css")] + [self.bundledCSS(fname) for fname in css]
)
jstxt = "\n".join(
[self.bundledScript("webview.js")]
+ [self.bundledScript(fname) for fname in js]
)
csstxt = "\n".join(self.bundledCSS(fname) for fname in web_content.css)
jstxt = "\n".join(self.bundledScript(fname) for fname in web_content.js)
from aqt import mw
head = mw.baseHTML() + head + csstxt + jstxt
head = mw.baseHTML() + web_content.head + csstxt + jstxt
body_class = theme_manager.body_class()
@ -336,20 +403,20 @@ body {{ zoom: {}; background: {}; {} }}
widgetspec,
head,
body_class,
body,
web_content.body,
)
# print(html)
self.setHtml(html)
def webBundlePath(self, path):
def webBundlePath(self, path: str) -> str:
from aqt import mw
return "http://127.0.0.1:%d/_anki/%s" % (mw.mediaServer.getPort(), path)
return "http://127.0.0.1:%d/%s" % (mw.mediaServer.getPort(), path)
def bundledScript(self, fname):
def bundledScript(self, fname: str) -> str:
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(
fname
)

View File

@ -163,6 +163,29 @@ hooks = [
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:
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
web_content.js.append("my_addon.js")
web_content.css.append("my_addon.css")
web_content.head += "<script>console.log('my_addon')</script>"
web_content.body += "<div id='my-addon'></div>"
""",
),
Hook(
name="webview_will_show_context_menu",
args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"],