From 7fcb6b56722ad6dc0ef09450a4e5201ed0e16f4f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 9 Feb 2020 08:59:29 +1000 Subject: [PATCH] pass instance to webview_did_receive_js_message instead of string --- qt/aqt/browser.py | 19 +++++++++++++++---- qt/aqt/clayout.py | 4 ++-- qt/aqt/deckbrowser.py | 14 ++++++++++++-- qt/aqt/editor.py | 2 +- qt/aqt/gui_hooks.py | 40 +++++++++++++++++++++++++++++----------- qt/aqt/main.py | 11 +++++++++-- qt/aqt/overview.py | 9 +++++++-- qt/aqt/reviewer.py | 11 +++++++++-- qt/aqt/toolbar.py | 16 ++++++++++++++-- qt/aqt/webview.py | 12 ++++++------ qt/tools/genhooks_gui.py | 32 +++++++++++++++++++++++++------- 11 files changed, 129 insertions(+), 41 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index e5333986b..36026759c 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -49,6 +49,15 @@ from aqt.utils import ( ) from aqt.webview import AnkiWebView + +class PreviewDialog(QDialog): + pass + + +class FindDupesDialog(QDialog): + pass + + # Data model ########################################################################## @@ -1536,7 +1545,7 @@ where id in %s""" def _openPreview(self): self._previewState = "question" self._lastPreviewState = None - self._previewWindow = QDialog(None, Qt.Window) + self._previewWindow = PreviewDialog(None, Qt.Window) self._previewWindow.setWindowTitle(_("Preview")) self._previewWindow.finished.connect(self._onPreviewFinished) @@ -1641,7 +1650,9 @@ where id in %s""" self._previewWeb.stdHtml( 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: if cmd.startswith("play:"): @@ -2118,7 +2129,7 @@ update cards set usn=?, mod=?, did=? where id in """ self.editor.saveNow(self._onFindDupes) def _onFindDupes(self): - d = QDialog(self) + d = FindDupesDialog(self) self.mw.setupDialogGC(d) frm = aqt.forms.finddupes.Ui_Dialog() frm.setupUi(d) @@ -2129,7 +2140,7 @@ update cards set usn=?, mod=?, did=? where id in """ frm.fields.addItems(fields) self._dupesButton = None # links - frm.webView.set_bridge_command(self.dupeLinkClicked, "find_dupes") + frm.webView.set_bridge_command(self.dupeLinkClicked, d) def onFin(code): saveGeom(d, "findDupes") diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 37cad38a2..80f0436e8 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -220,8 +220,8 @@ class CardLayout(QDialog): pform.backWeb.stdHtml( self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc ) - pform.frontWeb.set_bridge_command(self._on_bridge_cmd, "card_layout") - pform.backWeb.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, self) def _on_bridge_cmd(self, cmd: str) -> Any: if cmd.startswith("play:"): diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index f4cdc8dfe..2857ef705 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -1,6 +1,9 @@ # Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + from copy import deepcopy from typing import Any @@ -15,6 +18,11 @@ from aqt.toolbar import BottomBar 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: _dueTree: Any @@ -26,7 +34,7 @@ class DeckBrowser: def show(self): 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() # redraw top bar for theme change self.mw.toolbar.draw() @@ -333,7 +341,9 @@ where id > ?""", b ) 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): openLink(aqt.appShared + "decks/") diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 2306de711..726db64e4 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -97,7 +97,7 @@ class Editor: self.web = EditorWebView(self.widget, self) self.web.title = "editor" 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) righttopbtns: List[str] = [ diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index bb752c83c..bd2fb71ea 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -1070,33 +1070,51 @@ 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. + Message is the string passed to pycmd(). + + 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).""" + 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: - _hooks: List[Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]]] = [] + 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( - self, cb: Callable[[Tuple[bool, Any], str, str], Tuple[bool, Any]] + self, cb: Callable[[Tuple[bool, Any], str, Any], Tuple[bool, Any]] ) -> None: - """(handled: Tuple[bool, Any], message: str, context: str)""" + """(handled: Tuple[bool, Any], message: str, context: Any)""" self._hooks.append(cb) 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: if cb in self._hooks: self._hooks.remove(cb) def __call__( - self, handled: Tuple[bool, Any], message: str, context: str + self, handled: Tuple[bool, Any], message: str, context: Any ) -> Tuple[bool, Any]: for filter in self._hooks: try: diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 10d68772d..1b7acff8b 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + import faulthandler import gc import os @@ -58,6 +60,11 @@ from aqt.utils import ( install_pylib_legacy() +class ResetRequired: + def __init__(self, mw: AnkiQt): + self.mw = mw + + class AnkiQt(QMainWindow): col: _Collection pm: ProfileManagerType @@ -647,14 +654,14 @@ from the profile screen." # windows self.progress.timer(100, self.maybeReset, False) - def _resetRequiredState(self, oldState): + def _resetRequiredState(self, oldState: str) -> None: if oldState != "resetRequired": self.returnState = oldState 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(), "reset_required" + lambda url: self.delayedMaybeReset(), ResetRequired(self) ) i = _("Waiting for editing to finish.") b = self.button("refresh", _("Resume Now"), id="resume") diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 6b0b98dd1..2814a4ba2 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -12,6 +12,11 @@ from aqt.toolbar import BottomBar from aqt.utils import askUserDialog, openLink, shortcut, tooltip +class OverviewBottomBar: + def __init__(self, overview: Overview): + self.overview = overview + + class Overview: "Deck overview." @@ -22,7 +27,7 @@ class Overview: def show(self): 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.refresh() @@ -239,7 +244,7 @@ to their original deck.""" b ) 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 ###################################################################### diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 426296f2b..a25ab5901 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -2,6 +2,8 @@ # 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 difflib import html import html.parser @@ -22,6 +24,11 @@ from aqt.toolbar import BottomBar from aqt.utils import askUserDialog, downArrow, qtMenuShortcutWorkaround, tooltip +class ReviewerBottomBar: + def __init__(self, reviewer: Reviewer): + self.reviewer = reviewer + + class Reviewer: "Manage reviews. Maintains a separate state." @@ -41,8 +48,8 @@ class Reviewer: def show(self): self.mw.col.reset() self.mw.setStateShortcuts(self._shortcutKeys()) - self.web.set_bridge_command(self._linkHandler, "reviewer") - self.bottom.web.set_bridge_command(self._linkHandler, "reviewer_bottom") + self.web.set_bridge_command(self._linkHandler, self) + self.bottom.web.set_bridge_command(self._linkHandler, ReviewerBottomBar(self)) self._reps = None self.nextCard() diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 6d35b6172..4d3b6b9ae 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -10,6 +10,18 @@ from aqt.qt import * 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: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: self.mw = mw @@ -26,7 +38,7 @@ class Toolbar: self.web.requiresCol = False 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.adjustHeightToFit() @@ -112,7 +124,7 @@ class BottomBar(Toolbar): def draw(self, buf): # 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._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"] ) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 410363a55..8a2df4107 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -414,7 +414,7 @@ body {{ zoom: {}; background: {}; {} }} self._maybeRunActions() else: handled, result = gui_hooks.webview_did_receive_js_message( - (False, None), cmd, self._bridge_command_name + (False, None), cmd, self._bridge_context ) if handled: return result @@ -427,7 +427,7 @@ body {{ zoom: {}; background: {}; {} }} # legacy def resetHandlers(self): self.onBridgeCmd = self.defaultOnBridgeCmd - self._bridge_command_name = "unknown" + self._bridge_context = None def adjustHeightToFit(self): self.evalWithCallback("$(document.body).height()", self._onHeight) @@ -447,10 +447,10 @@ body {{ zoom: {}; background: {}; {} }} height = math.ceil(qvar * scaleFactor) 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. - Context is a human readable name that is provided to the - webview_did_receive_js_message hook.""" + Context is the object calling this routine, eg an instance of + aqt.reviewer.Reviewer or aqt.deckbrowser.DeckBrowser.""" self.onBridgeCmd = func - self._bridge_command_name = context + self._bridge_context = context diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index a146519e9..56268f914 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -130,20 +130,38 @@ hooks = [ ################### Hook( 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]", 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. + Message is the string passed to pycmd(). + + 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).""", + 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( name="webview_will_show_context_menu",