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): def _setupPreviewWebview(self):
jsinc = [ jsinc = [
"jquery.js", "_anki/jquery.js",
"browsersel.js", "_anki/browsersel.js",
"mathjax/conf.js", "_anki/mathjax/conf.js",
"mathjax/MathJax.js", "_anki/mathjax/MathJax.js",
"reviewer.js", "_anki/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=["_anki/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:
@ -2159,10 +2162,9 @@ update cards set usn=?, mod=?, did=? where id in """
self._dupesButton = None self._dupesButton = None
# links # links
frm.webView.title = "find duplicates" frm.webView.title = "find duplicates"
frm.webView.set_bridge_command( web_context = FindDupesDialog(dialog=d, browser=self)
self.dupeLinkClicked, FindDupesDialog(dialog=d, browser=self) frm.webView.set_bridge_command(self.dupeLinkClicked, web_context)
) frm.webView.stdHtml("", context=web_context)
frm.webView.stdHtml("")
def onFin(code): def onFin(code):
saveGeom(d, "findDupes") saveGeom(d, "findDupes")
@ -2171,13 +2173,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:
@ -2202,7 +2206,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):

View File

@ -208,17 +208,17 @@ class CardLayout(QDialog):
pform.backWeb = AnkiWebView(title="card layout back") pform.backWeb = AnkiWebView(title="card layout back")
pform.backPrevBox.addWidget(pform.backWeb) pform.backPrevBox.addWidget(pform.backWeb)
jsinc = [ jsinc = [
"jquery.js", "_anki/jquery.js",
"browsersel.js", "_anki/browsersel.js",
"mathjax/conf.js", "_anki/mathjax/conf.js",
"mathjax/MathJax.js", "_anki/mathjax/MathJax.js",
"reviewer.js", "_anki/reviewer.js",
] ]
pform.frontWeb.stdHtml( 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( 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.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

@ -107,8 +107,9 @@ class DeckBrowser:
stats = self._renderStats() stats = self._renderStats()
self.web.stdHtml( self.web.stdHtml(
self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()), self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()),
css=["deckbrowser.css"], css=["_anki/deckbrowser.css"],
js=["jquery.js", "jquery-ui.js", "deckbrowser.js"], js=["_anki/jquery.js", "_anki/jquery-ui.js", "_anki/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

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

View File

@ -1129,6 +1129,56 @@ 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:
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: 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()

View File

@ -149,8 +149,9 @@ class Overview:
desc=self._desc(deck), desc=self._desc(deck),
table=self._table(), table=self._table(),
), ),
css=["overview.css"], css=["_anki/overview.css"],
js=["jquery.js", "overview.js"], js=["_anki/jquery.js", "_anki/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

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

View File

@ -97,6 +97,8 @@ class DeckStats(QDialog):
self.report = stats.report(type=self.period) self.report = stats.report(type=self.period)
self.form.web.title = "deck stats" 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=["_anki/jquery.js", "_anki/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 Optional, Any
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=["_anki/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=["_anki/toolbar.css", "_anki/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,6 +97,64 @@ 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>"
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 # Main web view
########################################################################## ##########################################################################
@ -256,11 +315,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=["_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() palette = self.style().standardPalette()
color_hl = palette.color(QPalette.Highlight).name() color_hl = palette.color(QPalette.Highlight).name()
@ -301,16 +372,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()
@ -336,20 +403,20 @@ 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) 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) 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,29 @@ 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:
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( 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"],