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:
commit
990a6c394b
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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]] = []
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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"],
|
||||||
|
Loading…
Reference in New Issue
Block a user