mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add a webview_did_receive_js_message hook
This commit is contained in:
parent
a5db36e208
commit
d54f719558
11 changed files with 131 additions and 38 deletions
|
@ -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")
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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/")
|
||||
|
|
|
@ -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] = [
|
||||
|
|
|
@ -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]] = []
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
######################################################################
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in a new issue