add a webview_did_receive_js_message hook

This commit is contained in:
Damien Elmes 2020-01-22 10:46:35 +10:00
parent a5db36e208
commit d54f719558
11 changed files with 131 additions and 38 deletions

View file

@ -2133,7 +2133,7 @@ update cards set usn=?, mod=?, did=? where id in """
frm.fields.addItems(fields)
self._dupesButton = None
# links
frm.webView.onBridgeCmd = self.dupeLinkClicked
frm.webView.set_bridge_command(self.dupeLinkClicked, "find_dupes")
def onFin(code):
saveGeom(d, "findDupes")

View file

@ -216,6 +216,9 @@ class CardLayout(QDialog):
pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
)
# specify a context for add-ons
pform.frontWeb.set_bridge_command(lambda msg: None, "card_layout")
pform.backWeb.set_bridge_command(lambda msg: None, "card_layout")
def updateMainArea(self):
if self._isCloze():

View file

@ -8,25 +8,25 @@ import aqt
from anki.errors import DeckRenameError
from anki.lang import _, ngettext
from anki.utils import fmtTimeSpan, ids2str
from aqt import gui_hooks
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.sound import av_player
from aqt.toolbar import BottomBar
from aqt.utils import askUser, getOnlyText, openHelp, openLink, shortcut, showWarning
class DeckBrowser:
_dueTree: Any
def __init__(self, mw):
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
self.web = mw.web
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
self.bottom = BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0)
def show(self):
av_player.stop_and_clear_queue()
self.web.resetHandlers()
self.web.onBridgeCmd = self._linkHandler
self.web.set_bridge_command(self._linkHandler, "deck_browser")
self._renderPage()
def refresh(self):
@ -330,7 +330,7 @@ where id > ?""",
b
)
self.bottom.draw(buf)
self.bottom.web.onBridgeCmd = self._linkHandler
self.bottom.web.set_bridge_command(self._linkHandler, "deck_browser_bottom_bar")
def _onShared(self):
openLink(aqt.appShared + "decks/")

View file

@ -96,7 +96,7 @@ class Editor:
self.web = EditorWebView(self.widget, self)
self.web.title = "editor"
self.web.allowDrops = True
self.web.onBridgeCmd = self.onBridgeCmd
self.web.set_bridge_command(self.onBridgeCmd, "editor")
self.outerLayout.addWidget(self.web, 1)
righttopbtns: List[str] = [

View file

@ -913,6 +913,50 @@ class _UndoStateDidChangeHook:
undo_state_did_change = _UndoStateDidChangeHook()
class _WebviewDidReceiveJsMessageFilter:
"""Used to handle pycmd() messages sent from Javascript.
Message is the string passed to pycmd(). Context is what was
passed to set_bridge_command(), such as 'editor' or 'reviewer'.
For messages you don't want to handle, return handled unchanged.
If you handle a message and don't want it passed to the original
bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value)."""
_hooks: List[Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]]] = []
def append(
self, cb: Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]]
) -> None:
"""(handled: Tuple[bool, Any], message: str, context: str)"""
self._hooks.append(cb)
def remove(
self, cb: Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]]
) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(
self, handled: Tuple[bool, Any], message: str, context: str
) -> Tuple[bool, Any]:
for filter in self._hooks:
try:
handled = filter(handled, message, context)
except:
# if the hook fails, remove it
self._hooks.remove(filter)
raise
return handled
webview_did_receive_js_message = _WebviewDidReceiveJsMessageFilter()
class _WebviewWillShowContextMenuHook:
_hooks: List[Callable[["aqt.webview.AnkiWebView", QMenu], None]] = []

View file

@ -643,8 +643,9 @@ from the profile screen."
if self.resetModal:
# we don't have to change the webview, as we have a covering window
return
self.web.resetHandlers()
self.web.onBridgeCmd = lambda url: self.delayedMaybeReset()
self.web.set_bridge_command(
lambda url: self.delayedMaybeReset(), "reset_required"
)
i = _("Waiting for editing to finish.")
b = self.button("refresh", _("Resume Now"), id="resume")
self.web.stdHtml(

View file

@ -2,24 +2,26 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import aqt
from anki.lang import _
from aqt.sound import av_player
from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, openLink, shortcut, tooltip
class Overview:
"Deck overview."
def __init__(self, mw):
def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw
self.web = mw.web
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
self.bottom = BottomBar(mw, mw.bottomWeb)
def show(self):
av_player.stop_and_clear_queue()
self.web.resetHandlers()
self.web.onBridgeCmd = self._linkHandler
self.web.set_bridge_command(self._linkHandler, "overview")
self.mw.setStateShortcuts(self._shortcutKeys())
self.refresh()
@ -235,7 +237,7 @@ to their original deck."""
b
)
self.bottom.draw(buf)
self.bottom.web.onBridgeCmd = self._linkHandler
self.bottom.web.set_bridge_command(self._linkHandler, "overview_bottom")
# Studying more
######################################################################

View file

@ -10,7 +10,6 @@ import re
import unicodedata as ucd
from typing import List, Optional
import aqt
from anki import hooks
from anki.cards import Card
from anki.lang import _, ngettext
@ -19,6 +18,7 @@ from anki.utils import bodyClass, stripHTML
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.sound import av_player, getAudio
from aqt.toolbar import BottomBar
from aqt.utils import (
askUserDialog,
downArrow,
@ -42,15 +42,14 @@ class Reviewer:
self._current_side_audio: Optional[List[AVTag]] = None
self.typeCorrect = None # web init happens before this is set
self.state: Optional[str] = None
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
self.bottom = BottomBar(mw, mw.bottomWeb)
hooks.card_did_leech.append(self.onLeech)
def show(self):
self.mw.col.reset()
self.web.resetHandlers()
self.mw.setStateShortcuts(self._shortcutKeys())
self.web.onBridgeCmd = self._linkHandler
self.bottom.web.onBridgeCmd = self._linkHandler
self.web.set_bridge_command(self._linkHandler, "reviewer")
self.bottom.web.set_bridge_command(self._linkHandler, "reviewer_bottom")
self._reps = None
self.nextCard()

View file

@ -2,12 +2,16 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import aqt
from anki.lang import _
from aqt.qt import *
from aqt.webview import AnkiWebView
class Toolbar:
def __init__(self, mw, web):
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
self.mw = mw
self.web = web
self.link_handlers = {
@ -22,7 +26,7 @@ class Toolbar:
self.web.requiresCol = False
def draw(self):
self.web.onBridgeCmd = self._linkHandler
self.web.set_bridge_command(self._linkHandler, "top_toolbar")
self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"])
self.web.adjustHeightToFit()
@ -107,7 +111,8 @@ class BottomBar(Toolbar):
"""
def draw(self, buf):
self.web.onBridgeCmd = self._linkHandler
# note: some screens may override this
self.web.set_bridge_command(self._linkHandler, "bottom_toolbar")
self.web.stdHtml(
self._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"]
)

View file

@ -4,6 +4,7 @@
import json
import math
import sys
from typing import Any, List, Optional, Tuple
from anki.lang import _
from anki.utils import isLin, isMac, isWin
@ -99,18 +100,21 @@ class AnkiWebPage(QWebEnginePage): # type: ignore
class AnkiWebView(QWebEngineView): # type: ignore
def __init__(self, parent=None):
QWebEngineView.__init__(self, parent=parent)
def __init__(self, parent: Optional[QWidget] = None) -> None:
QWebEngineView.__init__(self, parent=parent) # type: ignore
self.title = "default"
self._page = AnkiWebPage(self._onBridgeCmd)
self._page.setBackgroundColor(self._getWindowColor()) # reduce flicker
# in new code, use .set_bridge_command() instead of setting this directly
self.onBridgeCmd: Callable[[str], Any] = self.defaultOnBridgeCmd
self._domDone = True
self._pendingActions = []
self._pendingActions: List[Tuple[str, List[Any]]] = []
self.requiresCol = True
self.setPage(self._page)
self._page.profile().setHttpCacheType(QWebEngineProfile.NoCache)
self._page.profile().setHttpCacheType(QWebEngineProfile.NoCache) # type: ignore
self.resetHandlers()
self.allowDrops = False
self._filterSet = False
@ -119,7 +123,7 @@ class AnkiWebView(QWebEngineView): # type: ignore
self,
context=Qt.WidgetWithChildrenShortcut,
activated=self.onEsc,
)
) # type: ignore
if isMac:
for key, fn in [
(QKeySequence.Copy, self.onCopy),
@ -129,13 +133,13 @@ class AnkiWebView(QWebEngineView): # type: ignore
]:
QShortcut(
key, self, context=Qt.WidgetWithChildrenShortcut, activated=fn
)
) # type: ignore
QShortcut(
QKeySequence("ctrl+shift+v"),
self,
context=Qt.WidgetWithChildrenShortcut,
activated=self.onPaste,
)
) # type: ignore
def eventFilter(self, obj, evt):
# disable pinch to zoom gesture
@ -391,7 +395,7 @@ body {{ zoom: {}; background: {}; {} }}
return True
return False
def _onBridgeCmd(self, cmd):
def _onBridgeCmd(self, cmd: str) -> Any:
if self._shouldIgnoreWebEvent():
print("ignored late bridge cmd", cmd)
return
@ -404,13 +408,21 @@ body {{ zoom: {}; background: {}; {} }}
self._domDone = True
self._maybeRunActions()
else:
return self.onBridgeCmd(cmd)
handled, result = gui_hooks.webview_did_receive_js_message(
(False, None), cmd, self._bridge_command_name
)
if handled:
return result
else:
return self.onBridgeCmd(cmd)
def defaultOnBridgeCmd(self, cmd):
def defaultOnBridgeCmd(self, cmd: str) -> Any:
print("unhandled bridge cmd:", cmd)
# legacy
def resetHandlers(self):
self.onBridgeCmd = self.defaultOnBridgeCmd
self._bridge_command_name = "unknown"
def adjustHeightToFit(self):
self.evalWithCallback("$(document.body).height()", self._onHeight)
@ -429,3 +441,11 @@ body {{ zoom: {}; background: {}; {} }}
height = math.ceil(qvar * scaleFactor)
self.setFixedHeight(height)
def set_bridge_command(self, func: Callable[[str], Any], context: str) -> None:
"""Set a handler for pycmd() messages received from Javascript.
Context is a human readable name that is provided to the
webview_did_receive_js_message hook."""
self.onBridgeCmd = func
self._bridge_command_name = context

View file

@ -65,11 +65,6 @@ hooks = [
args=["browser: aqt.browser.Browser"],
legacy_hook="browser.rowChanged",
),
Hook(
name="webview_will_show_context_menu",
args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"],
legacy_hook="AnkiWebView.contextMenuEvent",
),
# States
###################
Hook(
@ -98,6 +93,30 @@ hooks = [
legacy_hook="reset",
doc="Called when the interface needs to be redisplayed after non-trivial changes have been made.",
),
# Webview
###################
Hook(
name="webview_did_receive_js_message",
args=["handled: Tuple[bool, Any]", "message: str", "context: str"],
return_type="Tuple[bool, Any]",
doc="""Used to handle pycmd() messages sent from Javascript.
Message is the string passed to pycmd(). Context is what was
passed to set_bridge_command(), such as 'editor' or 'reviewer'.
For messages you don't want to handle, return handled unchanged.
If you handle a message and don't want it passed to the original
bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value).""",
),
Hook(
name="webview_will_show_context_menu",
args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"],
legacy_hook="AnkiWebView.contextMenuEvent",
),
# Main
###################
Hook(name="profile_did_open", legacy_hook="profileLoaded"),