Add webview_will_set_content hook & update supporting code accordingly
This commit is contained in:
parent
df2a7b06ef
commit
bbd667b0ff
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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]] = []
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
######################################################################
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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"],
|
||||
|
Loading…
Reference in New Issue
Block a user