pass instance to webview_did_receive_js_message instead of string

This commit is contained in:
Damien Elmes 2020-02-09 08:59:29 +10:00
parent 5bd67509ae
commit 7fcb6b5672
11 changed files with 129 additions and 41 deletions

View file

@ -49,6 +49,15 @@ from aqt.utils import (
) )
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
class PreviewDialog(QDialog):
pass
class FindDupesDialog(QDialog):
pass
# Data model # Data model
########################################################################## ##########################################################################
@ -1536,7 +1545,7 @@ where id in %s"""
def _openPreview(self): def _openPreview(self):
self._previewState = "question" self._previewState = "question"
self._lastPreviewState = None self._lastPreviewState = None
self._previewWindow = QDialog(None, Qt.Window) self._previewWindow = PreviewDialog(None, Qt.Window)
self._previewWindow.setWindowTitle(_("Preview")) self._previewWindow.setWindowTitle(_("Preview"))
self._previewWindow.finished.connect(self._onPreviewFinished) self._previewWindow.finished.connect(self._onPreviewFinished)
@ -1641,7 +1650,9 @@ where id in %s"""
self._previewWeb.stdHtml( self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
) )
self._previewWeb.set_bridge_command(self._on_preview_bridge_cmd, "preview") self._previewWeb.set_bridge_command(
self._on_preview_bridge_cmd, self._previewWindow
)
def _on_preview_bridge_cmd(self, cmd: str) -> Any: def _on_preview_bridge_cmd(self, cmd: str) -> Any:
if cmd.startswith("play:"): if cmd.startswith("play:"):
@ -2118,7 +2129,7 @@ update cards set usn=?, mod=?, did=? where id in """
self.editor.saveNow(self._onFindDupes) self.editor.saveNow(self._onFindDupes)
def _onFindDupes(self): def _onFindDupes(self):
d = QDialog(self) d = FindDupesDialog(self)
self.mw.setupDialogGC(d) self.mw.setupDialogGC(d)
frm = aqt.forms.finddupes.Ui_Dialog() frm = aqt.forms.finddupes.Ui_Dialog()
frm.setupUi(d) frm.setupUi(d)
@ -2129,7 +2140,7 @@ 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(self.dupeLinkClicked, "find_dupes") frm.webView.set_bridge_command(self.dupeLinkClicked, d)
def onFin(code): def onFin(code):
saveGeom(d, "findDupes") saveGeom(d, "findDupes")

View file

@ -220,8 +220,8 @@ class CardLayout(QDialog):
pform.backWeb.stdHtml( pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
) )
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, "card_layout") pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self)
pform.backWeb.set_bridge_command(self._on_bridge_cmd, "card_layout") pform.backWeb.set_bridge_command(self._on_bridge_cmd, self)
def _on_bridge_cmd(self, cmd: str) -> Any: def _on_bridge_cmd(self, cmd: str) -> Any:
if cmd.startswith("play:"): if cmd.startswith("play:"):

View file

@ -1,6 +1,9 @@
# 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
from __future__ import annotations
from copy import deepcopy from copy import deepcopy
from typing import Any from typing import Any
@ -15,6 +18,11 @@ from aqt.toolbar import BottomBar
from aqt.utils import askUser, getOnlyText, openHelp, openLink, shortcut, showWarning from aqt.utils import askUser, getOnlyText, openHelp, openLink, shortcut, showWarning
class DeckBrowserBottomBar:
def __init__(self, deck_browser: DeckBrowser):
self.deck_browser = deck_browser
class DeckBrowser: class DeckBrowser:
_dueTree: Any _dueTree: Any
@ -26,7 +34,7 @@ class DeckBrowser:
def show(self): def show(self):
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, "deck_browser") self.web.set_bridge_command(self._linkHandler, self)
self._renderPage() self._renderPage()
# redraw top bar for theme change # redraw top bar for theme change
self.mw.toolbar.draw() self.mw.toolbar.draw()
@ -333,7 +341,9 @@ where id > ?""",
b b
) )
self.bottom.draw(buf) self.bottom.draw(buf)
self.bottom.web.set_bridge_command(self._linkHandler, "deck_browser_bottom_bar") self.bottom.web.set_bridge_command(
self._linkHandler, DeckBrowserBottomBar(self)
)
def _onShared(self): def _onShared(self):
openLink(aqt.appShared + "decks/") openLink(aqt.appShared + "decks/")

View file

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

View file

@ -1070,33 +1070,51 @@ undo_state_did_change = _UndoStateDidChangeHook()
class _WebviewDidReceiveJsMessageFilter: class _WebviewDidReceiveJsMessageFilter:
"""Used to handle pycmd() messages sent from Javascript. """Used to handle pycmd() messages sent from Javascript.
Message is the string passed to pycmd(). Context is what was Message is the string passed to pycmd().
passed to set_bridge_command(), such as 'editor' or 'reviewer'.
For messages you don't want to handle, return handled unchanged. 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 If you handle a message and don't want it passed to the original
bridge command handler, return (True, None). bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value).""" return it with (True, some_value).
_hooks: List[Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]]] = [] Context is the instance that was passed to set_bridge_command().
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, pass on message
return handled
if message == "my-mark-action":
# our message, call onMark() on the reviewer instance
context.onMark()
# and don't pass message to other handlers
return (True, None)
else:
# some other command, pass it on
return handled
"""
_hooks: List[Callable[[Tuple[bool, Any], str, Any], Tuple[bool, Any]]] = []
def append( def append(
self, cb: Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]] self, cb: Callable[[Tuple[bool, Any], str, Any], Tuple[bool, Any]]
) -> None: ) -> None:
"""(handled: Tuple[bool, Any], message: str, context: str)""" """(handled: Tuple[bool, Any], message: str, context: Any)"""
self._hooks.append(cb) self._hooks.append(cb)
def remove( def remove(
self, cb: Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]] self, cb: Callable[[Tuple[bool, Any], str, Any], Tuple[bool, Any]]
) -> None: ) -> None:
if cb in self._hooks: if cb in self._hooks:
self._hooks.remove(cb) self._hooks.remove(cb)
def __call__( def __call__(
self, handled: Tuple[bool, Any], message: str, context: str self, handled: Tuple[bool, Any], message: str, context: Any
) -> Tuple[bool, Any]: ) -> Tuple[bool, Any]:
for filter in self._hooks: for filter in self._hooks:
try: try:

View file

@ -2,6 +2,8 @@
# -*- 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
from __future__ import annotations
import faulthandler import faulthandler
import gc import gc
import os import os
@ -58,6 +60,11 @@ from aqt.utils import (
install_pylib_legacy() install_pylib_legacy()
class ResetRequired:
def __init__(self, mw: AnkiQt):
self.mw = mw
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
col: _Collection col: _Collection
pm: ProfileManagerType pm: ProfileManagerType
@ -647,14 +654,14 @@ from the profile screen."
# windows # windows
self.progress.timer(100, self.maybeReset, False) self.progress.timer(100, self.maybeReset, False)
def _resetRequiredState(self, oldState): def _resetRequiredState(self, oldState: str) -> None:
if oldState != "resetRequired": if oldState != "resetRequired":
self.returnState = oldState self.returnState = oldState
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( self.web.set_bridge_command(
lambda url: self.delayedMaybeReset(), "reset_required" lambda url: self.delayedMaybeReset(), ResetRequired(self)
) )
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")

View file

@ -12,6 +12,11 @@ from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, openLink, shortcut, tooltip from aqt.utils import askUserDialog, openLink, shortcut, tooltip
class OverviewBottomBar:
def __init__(self, overview: Overview):
self.overview = overview
class Overview: class Overview:
"Deck overview." "Deck overview."
@ -22,7 +27,7 @@ class Overview:
def show(self): def show(self):
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, "overview") self.web.set_bridge_command(self._linkHandler, self)
self.mw.setStateShortcuts(self._shortcutKeys()) self.mw.setStateShortcuts(self._shortcutKeys())
self.refresh() self.refresh()
@ -239,7 +244,7 @@ to their original deck."""
b b
) )
self.bottom.draw(buf) self.bottom.draw(buf)
self.bottom.web.set_bridge_command(self._linkHandler, "overview_bottom") self.bottom.web.set_bridge_command(self._linkHandler, OverviewBottomBar(self))
# Studying more # Studying more
###################################################################### ######################################################################

View file

@ -2,6 +2,8 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# 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
from __future__ import annotations
import difflib import difflib
import html import html
import html.parser import html.parser
@ -22,6 +24,11 @@ from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip
class ReviewerBottomBar:
def __init__(self, reviewer: Reviewer):
self.reviewer = reviewer
class Reviewer: class Reviewer:
"Manage reviews. Maintains a separate state." "Manage reviews. Maintains a separate state."
@ -41,8 +48,8 @@ class Reviewer:
def show(self): def show(self):
self.mw.col.reset() self.mw.col.reset()
self.mw.setStateShortcuts(self._shortcutKeys()) self.mw.setStateShortcuts(self._shortcutKeys())
self.web.set_bridge_command(self._linkHandler, "reviewer") self.web.set_bridge_command(self._linkHandler, self)
self.bottom.web.set_bridge_command(self._linkHandler, "reviewer_bottom") self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self))
self._reps = None self._reps = None
self.nextCard() self.nextCard()

View file

@ -10,6 +10,18 @@ from aqt.qt import *
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
# wrapper class for set_bridge_command()
class TopToolbar:
def __init__(self, toolbar: Toolbar):
self.toolbar = toolbar
# wrapper class for set_bridge_command()
class BottomToolbar:
def __init__(self, toolbar: Toolbar):
self.toolbar = toolbar
class Toolbar: class Toolbar:
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:
self.mw = mw self.mw = mw
@ -26,7 +38,7 @@ class Toolbar:
self.web.requiresCol = False self.web.requiresCol = False
def draw(self): def draw(self):
self.web.set_bridge_command(self._linkHandler, "top_toolbar") self.web.set_bridge_command(self._linkHandler, TopToolbar(self))
self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"]) self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"])
self.web.adjustHeightToFit() self.web.adjustHeightToFit()
@ -112,7 +124,7 @@ class BottomBar(Toolbar):
def draw(self, buf): def draw(self, buf):
# note: some screens may override this # note: some screens may override this
self.web.set_bridge_command(self._linkHandler, "bottom_toolbar") self.web.set_bridge_command(self._linkHandler, BottomToolbar(self))
self.web.stdHtml( self.web.stdHtml(
self._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"] self._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"]
) )

View file

@ -414,7 +414,7 @@ body {{ zoom: {}; background: {}; {} }}
self._maybeRunActions() self._maybeRunActions()
else: else:
handled, result = gui_hooks.webview_did_receive_js_message( handled, result = gui_hooks.webview_did_receive_js_message(
(False, None), cmd, self._bridge_command_name (False, None), cmd, self._bridge_context
) )
if handled: if handled:
return result return result
@ -427,7 +427,7 @@ body {{ zoom: {}; background: {}; {} }}
# legacy # legacy
def resetHandlers(self): def resetHandlers(self):
self.onBridgeCmd = self.defaultOnBridgeCmd self.onBridgeCmd = self.defaultOnBridgeCmd
self._bridge_command_name = "unknown" self._bridge_context = None
def adjustHeightToFit(self): def adjustHeightToFit(self):
self.evalWithCallback("$(document.body).height()", self._onHeight) self.evalWithCallback("$(document.body).height()", self._onHeight)
@ -447,10 +447,10 @@ body {{ zoom: {}; background: {}; {} }}
height = math.ceil(qvar * scaleFactor) height = math.ceil(qvar * scaleFactor)
self.setFixedHeight(height) self.setFixedHeight(height)
def set_bridge_command(self, func: Callable[[str], Any], context: str) -> None: def set_bridge_command(self, func: Callable[[str], Any], context: Any) -> None:
"""Set a handler for pycmd() messages received from Javascript. """Set a handler for pycmd() messages received from Javascript.
Context is a human readable name that is provided to the Context is the object calling this routine, eg an instance of
webview_did_receive_js_message hook.""" aqt.reviewer.Reviewer or aqt.deckbrowser.DeckBrowser."""
self.onBridgeCmd = func self.onBridgeCmd = func
self._bridge_command_name = context self._bridge_context = context

View file

@ -130,20 +130,38 @@ hooks = [
################### ###################
Hook( Hook(
name="webview_did_receive_js_message", name="webview_did_receive_js_message",
args=["handled: Tuple[bool, Any]", "message: str", "context: str"], args=["handled: Tuple[bool, Any]", "message: str", "context: Any"],
return_type="Tuple[bool, Any]", return_type="Tuple[bool, Any]",
doc="""Used to handle pycmd() messages sent from Javascript. doc="""Used to handle pycmd() messages sent from Javascript.
Message is the string passed to pycmd(). Context is what was Message is the string passed to pycmd().
passed to set_bridge_command(), such as 'editor' or 'reviewer'.
For messages you don't want to handle, return handled unchanged. 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 If you handle a message and don't want it passed to the original
bridge command handler, return (True, None). bridge command handler, return (True, None).
If you want to pass a value to pycmd's result callback, you can If you want to pass a value to pycmd's result callback, you can
return it with (True, some_value).""", return it with (True, some_value).
Context is the instance that was passed to set_bridge_command().
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, pass on message
return handled
if message == "my-mark-action":
# our message, call onMark() on the reviewer instance
context.onMark()
# and don't pass message to other handlers
return (True, None)
else:
# some other command, pass it on
return handled
""",
), ),
Hook( Hook(
name="webview_will_show_context_menu", name="webview_will_show_context_menu",