From df2a7b06ef7774258b49f6b56f89ae5fcf83f1d5 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Wed, 12 Feb 2020 21:03:11 +0100 Subject: [PATCH 1/7] Refactor web view title setting and add titles to all web views Simplifies debugging web views --- qt/aqt/browser.py | 5 +++-- qt/aqt/clayout.py | 4 ++-- qt/aqt/editor.py | 3 +-- qt/aqt/main.py | 9 +++------ qt/aqt/stats.py | 1 + qt/aqt/webview.py | 6 ++++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 4b741a463..cd31deb6c 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1392,7 +1392,7 @@ by clicking on one on the left.""" d = CardInfoDialog(self) l = QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) - w = AnkiWebView() + w = AnkiWebView(title="browser card info") l.addWidget(w) w.stdHtml(info + "

" + reps) bb = QDialogButtonBox(QDialogButtonBox.Close) @@ -1561,7 +1561,7 @@ where id in %s""" self._previewWindow.silentlyClose = True vbox = QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) - self._previewWeb = AnkiWebView() + self._previewWeb = AnkiWebView(title="previewer") vbox.addWidget(self._previewWeb) bbox = QDialogButtonBox() @@ -2158,6 +2158,7 @@ update cards set usn=?, mod=?, did=? where id in """ frm.fields.addItems(fields) self._dupesButton = None # links + frm.webView.title = "find duplicates" frm.webView.set_bridge_command( self.dupeLinkClicked, FindDupesDialog(dialog=d, browser=self) ) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 80f0436e8..51e595fe0 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -203,9 +203,9 @@ class CardLayout(QDialog): def setupWebviews(self): pform = self.pform - pform.frontWeb = AnkiWebView() + pform.frontWeb = AnkiWebView(title="card layout front") pform.frontPrevBox.addWidget(pform.frontWeb) - pform.backWeb = AnkiWebView() + pform.backWeb = AnkiWebView(title="card layout back") pform.backPrevBox.addWidget(pform.backWeb) jsinc = [ "jquery.js", diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 0a77d4ca1..8ca4c2bf8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -95,7 +95,6 @@ class Editor: def setupWeb(self) -> None: self.web = EditorWebView(self.widget, self) - self.web.title = "editor" self.web.allowDrops = True self.web.set_bridge_command(self.onBridgeCmd, self) self.outerLayout.addWidget(self.web, 1) @@ -937,7 +936,7 @@ to a cloze type first, via Edit>Change Note Type.""" class EditorWebView(AnkiWebView): def __init__(self, parent, editor): - AnkiWebView.__init__(self) + AnkiWebView.__init__(self, title="editor") self.editor = editor self.strip = self.editor.mw.pm.profile["stripHTML"] self.setAcceptDrops(True) diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 1cc8be70b..d33ba1845 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -717,19 +717,16 @@ title="%s" %s>%s""" % ( self.form = aqt.forms.main.Ui_MainWindow() self.form.setupUi(self) # toolbar - tweb = self.toolbarWeb = aqt.webview.AnkiWebView() - tweb.title = "top toolbar" + tweb = self.toolbarWeb = aqt.webview.AnkiWebView(title="top toolbar") tweb.setFocusPolicy(Qt.WheelFocus) self.toolbar = aqt.toolbar.Toolbar(self, tweb) self.toolbar.draw() # main area - self.web = aqt.webview.AnkiWebView() - self.web.title = "main webview" + self.web = aqt.webview.AnkiWebView(title="main webview") self.web.setFocusPolicy(Qt.WheelFocus) self.web.setMinimumWidth(400) # bottom area - sweb = self.bottomWeb = aqt.webview.AnkiWebView() - sweb.title = "bottom toolbar" + sweb = self.bottomWeb = aqt.webview.AnkiWebView(title="bottom toolbar") sweb.setFocusPolicy(Qt.WheelFocus) # add in a layout self.mainLayout = QVBoxLayout() diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 8a6a2adf6..a1c493e3f 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -95,6 +95,7 @@ class DeckStats(QDialog): stats = self.mw.col.stats() stats.wholeCollection = self.wholeCollection self.report = stats.report(type=self.period) + self.form.web.title = "deck stats" self.form.web.stdHtml( "" + self.report + "", js=["jquery.js", "plot.js"] ) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 8a2df4107..e88ffc309 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -101,9 +101,11 @@ class AnkiWebPage(QWebEnginePage): # 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 - self.title = "default" + self.title = title self._page = AnkiWebPage(self._onBridgeCmd) self._page.setBackgroundColor(self._getWindowColor()) # reduce flicker From bbd667b0ffe18c05b1b12e17183c410ec1aa4fe3 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Wed, 12 Feb 2020 22:00:13 +0100 Subject: [PATCH 2/7] Add webview_will_set_content hook & update supporting code accordingly --- qt/aqt/browser.py | 34 +++++++------ qt/aqt/clayout.py | 14 +++--- qt/aqt/deckbrowser.py | 12 +++-- qt/aqt/editor.py | 5 +- qt/aqt/gui_hooks.py | 50 +++++++++++++++++++ qt/aqt/main.py | 8 +-- qt/aqt/overview.py | 10 ++-- qt/aqt/reviewer.py | 18 ++++--- qt/aqt/stats.py | 4 +- qt/aqt/toolbar.py | 32 +++++++++--- qt/aqt/webview.py | 103 ++++++++++++++++++++++++++++++++------- qt/tools/genhooks_gui.py | 23 +++++++++ 12 files changed, 243 insertions(+), 70 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index cd31deb6c..77d13ea70 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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 += "" - web.stdHtml(t) + web.stdHtml(t, context=web_context) self.mw.progress.finish() def _onTagDupes(self, res): diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 51e595fe0..9a01bcbe1 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -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) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 2857ef705..af4785254 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -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 > ?""", """ % 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): diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 8ca4c2bf8..257131351 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -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 diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index bd2fb71ea..1685b959f 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -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 += "" + web_content.body += "

" + """ + + _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]] = [] diff --git a/qt/aqt/main.py b/qt/aqt/main.py index d33ba1845..10c1e93f3 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -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 """ - % (i, b) + % (i, b), + context=web_context, ) self.bottomWeb.hide() self.web.setFocus() diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 2814a4ba2..e0500b612 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -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.""" """ % 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 ###################################################################### diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index a25ab5901..59f9c379b 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -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 diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index a1c493e3f..33f4c5168 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -97,6 +97,8 @@ class DeckStats(QDialog): self.report = stats.report(type=self.period) self.form.web.title = "deck stats" self.form.web.stdHtml( - "" + self.report + "", js=["jquery.js", "plot.js"] + "" + self.report + "", + js=["_anki/jquery.js", "_anki/plot.js"], + context=self, ) self.mw.progress.finish() diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 4d3b6b9ae..28a0a0222 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -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 """ - 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() diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index e88ffc309..1756797c9 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -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 += "" + web_content.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 '' % self.webBundlePath(fname) - def bundledCSS(self, fname): + def bundledCSS(self, fname: str) -> str: return '' % self.webBundlePath( fname ) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 56268f914..a766c270a 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -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 += "" + web_content.body += "
" + """, + ), Hook( name="webview_will_show_context_menu", args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"], From 5bd38ce0a5aa8af8e29d6fa374df072910b10f85 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Wed, 12 Feb 2020 22:12:45 +0100 Subject: [PATCH 3/7] Pass CardInfoDialog context to stdHtml --- qt/aqt/browser.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 77d13ea70..a43fa45f3 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1382,27 +1382,20 @@ by clicking on one on the left.""" info, cs = self._cardInfoData() reps = self._revlogData(cs) - class CardInfoDialog(QDialog): - silentlyClose = True - - def reject(self): - saveGeom(self, "revlog") - return QDialog.reject(self) - - d = CardInfoDialog(self) + card_info_dialog = CardInfoDialog(self) l = QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) w = AnkiWebView(title="browser card info") l.addWidget(w) - w.stdHtml(info + "

" + reps) + w.stdHtml(info + "

" + reps, context=card_info_dialog) bb = QDialogButtonBox(QDialogButtonBox.Close) l.addWidget(bb) - bb.rejected.connect(d.reject) - d.setLayout(l) - d.setWindowModality(Qt.WindowModal) - d.resize(500, 400) - restoreGeom(d, "revlog") - d.show() + bb.rejected.connect(card_info_dialog.reject) + card_info_dialog.setLayout(l) + card_info_dialog.setWindowModality(Qt.WindowModal) + card_info_dialog.resize(500, 400) + restoreGeom(card_info_dialog, "revlog", CardInfoDialog) + card_info_dialog.show() def _cardInfoData(self): from anki.stats import CardStats @@ -2480,3 +2473,18 @@ Are you sure you want to continue?""" def onHelp(self): 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) From c839cda19ff189faef3c19adfaa75c99aded81eb Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Wed, 12 Feb 2020 22:15:44 +0100 Subject: [PATCH 4/7] Fix missing "Optional" import and lint --- qt/aqt/browser.py | 1 + qt/aqt/clayout.py | 10 ++++++++-- qt/aqt/gui_hooks.py | 2 +- qt/aqt/toolbar.py | 6 ++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index a43fa45f3..5e8220ace 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -2478,6 +2478,7 @@ Are you sure you want to continue?""" # Card Info Dialog ###################################################################### + class CardInfoDialog(QDialog): silentlyClose = True diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 9a01bcbe1..5dbc9ce90 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -215,10 +215,16 @@ class CardLayout(QDialog): "_anki/reviewer.js", ] pform.frontWeb.stdHtml( - self.mw.reviewer.revHtml(), css=["_anki/reviewer.css"], js=jsinc, context=self + self.mw.reviewer.revHtml(), + css=["_anki/reviewer.css"], + js=jsinc, + context=self, ) pform.backWeb.stdHtml( - self.mw.reviewer.revHtml(), css=["_anki/reviewer.css"], js=jsinc, context=self + 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) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 1685b959f..0607930c8 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -7,7 +7,7 @@ See pylib/anki/hooks.py from __future__ import annotations -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple import anki import aqt diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 28a0a0222..a7ce013e2 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import Optional, Any +from typing import Any, Optional import aqt from anki.lang import _ @@ -49,7 +49,9 @@ class Toolbar: 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._body % self._centerLinks(), + css=["_anki/toolbar.css"], + context=web_context, ) self.web.adjustHeightToFit() From c86e55d4513a4e2b829554072e61576820b7eacb Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Wed, 12 Feb 2020 22:20:30 +0100 Subject: [PATCH 5/7] Fix "js" parameter type --- qt/aqt/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 2c57627da..2a16d69e2 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -219,5 +219,5 @@ suggestions, bug reports and donations." abt.label.setMinimumWidth(800) abt.label.setMinimumHeight(600) dialog.show() - abt.label.stdHtml(abouttext, js=" ") + abt.label.stdHtml(abouttext, js=[]) return dialog From 0e5dea4c9f6cf5a03dbdaa0890d863fd787e8f32 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Sat, 15 Feb 2020 15:03:43 +0100 Subject: [PATCH 6/7] Assume that web assets without a specified subpath are under /_anki Maintains compatibility with existing add-ons --- qt/aqt/browser.py | 12 ++++++------ qt/aqt/clayout.py | 20 +++++++------------- qt/aqt/deckbrowser.py | 4 ++-- qt/aqt/editor.py | 4 ++-- qt/aqt/overview.py | 4 ++-- qt/aqt/reviewer.py | 16 ++++++++-------- qt/aqt/stats.py | 2 +- qt/aqt/toolbar.py | 6 ++---- qt/aqt/webview.py | 11 ++++++++--- 9 files changed, 38 insertions(+), 41 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 5e8220ace..033f810c1 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1643,16 +1643,16 @@ where id in %s""" def _setupPreviewWebview(self): jsinc = [ - "_anki/jquery.js", - "_anki/browsersel.js", - "_anki/mathjax/conf.js", - "_anki/mathjax/MathJax.js", - "_anki/reviewer.js", + "jquery.js", + "browsersel.js", + "mathjax/conf.js", + "mathjax/MathJax.js", + "reviewer.js", ] web_context = PreviewDialog(dialog=self._previewWindow, browser=self) self._previewWeb.stdHtml( self.mw.reviewer.revHtml(), - css=["_anki/reviewer.css"], + css=["reviewer.css"], js=jsinc, context=web_context, ) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 5dbc9ce90..87084cd8b 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -208,23 +208,17 @@ class CardLayout(QDialog): pform.backWeb = AnkiWebView(title="card layout back") pform.backPrevBox.addWidget(pform.backWeb) jsinc = [ - "_anki/jquery.js", - "_anki/browsersel.js", - "_anki/mathjax/conf.js", - "_anki/mathjax/MathJax.js", - "_anki/reviewer.js", + "jquery.js", + "browsersel.js", + "mathjax/conf.js", + "mathjax/MathJax.js", + "reviewer.js", ] pform.frontWeb.stdHtml( - self.mw.reviewer.revHtml(), - css=["_anki/reviewer.css"], - js=jsinc, - context=self, + self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self, ) pform.backWeb.stdHtml( - self.mw.reviewer.revHtml(), - css=["_anki/reviewer.css"], - js=jsinc, - context=self, + self.mw.reviewer.revHtml(), css=["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) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index af4785254..a57647d62 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -107,8 +107,8 @@ class DeckBrowser: stats = self._renderStats() self.web.stdHtml( self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()), - css=["_anki/deckbrowser.css"], - js=["_anki/jquery.js", "_anki/jquery-ui.js", "_anki/deckbrowser.js"], + css=["deckbrowser.css"], + js=["jquery.js", "jquery-ui.js", "deckbrowser.js"], context=self, ) self.web.key = "deckBrowser" diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 257131351..b5974e371 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -164,8 +164,8 @@ class Editor: # then load page self.web.stdHtml( _html % (bgcol, bgcol, topbuts, _("Show Duplicates")), - css=["_anki/editor.css"], - js=["_anki/jquery.js", "_anki/editor.js"], + css=["editor.css"], + js=["jquery.js", "editor.js"], context=self, ) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index e0500b612..e94b69953 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -149,8 +149,8 @@ class Overview: desc=self._desc(deck), table=self._table(), ), - css=["_anki/overview.css"], - js=["_anki/jquery.js", "_anki/overview.js"], + css=["overview.css"], + js=["jquery.js", "overview.js"], context=self, ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 59f9c379b..9200b15f6 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -145,13 +145,13 @@ class Reviewer: # main window self.web.stdHtml( self.revHtml(), - css=["_anki/reviewer.css"], + css=["reviewer.css"], js=[ - "_anki/jquery.js", - "_anki/browsersel.js", - "_anki/mathjax/conf.js", - "_anki/mathjax/MathJax.js", - "_anki/reviewer.js", + "jquery.js", + "browsersel.js", + "mathjax/conf.js", + "mathjax/MathJax.js", + "reviewer.js", ], context=self, ) @@ -159,8 +159,8 @@ class Reviewer: self.bottom.web.show() self.bottom.web.stdHtml( self._bottomHTML(), - css=["_anki/toolbar-bottom.css", "_anki/reviewer-bottom.css"], - js=["_anki/jquery.js", "_anki/reviewer-bottom.js"], + css=["toolbar-bottom.css", "reviewer-bottom.css"], + js=["jquery.js", "reviewer-bottom.js"], context=ReviewerBottomBar(self), ) diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 33f4c5168..3a57d85f0 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -98,7 +98,7 @@ class DeckStats(QDialog): self.form.web.title = "deck stats" self.form.web.stdHtml( "" + self.report + "", - js=["_anki/jquery.js", "_anki/plot.js"], + js=["jquery.js", "plot.js"], context=self, ) self.mw.progress.finish() diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index a7ce013e2..088a371e0 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -49,9 +49,7 @@ class Toolbar: 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._body % self._centerLinks(), css=["toolbar.css"], context=web_context, ) self.web.adjustHeightToFit() @@ -147,7 +145,7 @@ class BottomBar(Toolbar): self.web.set_bridge_command(link_handler, web_context) self.web.stdHtml( self._centerBody % buf, - css=["_anki/toolbar.css", "_anki/toolbar-bottom.css"], + css=["toolbar.css", "toolbar-bottom.css"], context=web_context, ) self.web.adjustHeightToFit() diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 1756797c9..d001c2a1c 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -327,8 +327,8 @@ class AnkiWebView(QWebEngineView): # type: ignore 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), + 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) @@ -411,7 +411,12 @@ body {{ zoom: {}; background: {}; {} }} def webBundlePath(self, path: str) -> str: from aqt import mw - return "http://127.0.0.1:%d/%s" % (mw.mediaServer.getPort(), path) + if path.startswith("/"): + subpath = "" + else: + subpath = "/_anki/" + + return f"http://127.0.0.1:{mw.mediaServer.getPort()}{subpath}{path}" def bundledScript(self, fname: str) -> str: return '' % self.webBundlePath(fname) From 3637f466b4c7dea83fdd2a9bcab7c15b473ebe20 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Sat, 15 Feb 2020 15:03:58 +0100 Subject: [PATCH 7/7] Update documentation for webview_will_set_content and WebContent --- qt/aqt/gui_hooks.py | 25 ++++++++++++++++------ qt/aqt/webview.py | 46 +++++++++++++++++++++------------------- qt/tools/genhooks_gui.py | 25 ++++++++++++++++------ 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 0607930c8..623447870 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -1140,14 +1140,25 @@ class _WebviewWillSetContentHook: 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 + 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.js.append("my_addon.js") - web_content.css.append("my_addon.css") - web_content.head += "" - web_content.body += "

" + web_content.head += "" + web_content.body += "
" """ _hooks: List[Callable[["aqt.webview.WebContent", Optional[Any]], None]] = [] diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index d001c2a1c..52ff48026 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -103,50 +103,52 @@ class AnkiWebPage(QWebEnginePage): # type: ignore @dataclasses.dataclass class WebContent: - """Stores all dynamically modified content that a particular web view will be - populated with. + """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 + 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.: + - 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 += "" web_content.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. + - 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 components by utilizing - aqt.addons.AddonManager.setWebExports(). Web exports registered in this - manner may then be accessed under the `_addons` subpath. + 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 an `addon.js` and `addon.css` residing in a "web" - subfolder in your add-on package, first register the corresponding web export: + 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: + 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") + f"/_addons/{addon_package}/web/my-addon.css") web_content.js.append( - f"_addons/{addon_package}/web/addon.js") + f"/_addons/{addon_package}/web/my-addon.js") """ body: str = "" diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index a766c270a..d6210411c 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -176,14 +176,25 @@ hooks = [ 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 + 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.js.append("my_addon.js") - web_content.css.append("my_addon.css") - web_content.head += "" - web_content.body += "
" + web_content.head += "" + web_content.body += "
" """, ), Hook(