diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index c4c5aa258..bc6dd98e7 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -1294,6 +1294,41 @@ class _StyleDidInitFilter: style_did_init = _StyleDidInitFilter() +class _TopToolbarDidInitLinksHook: + """Used to modify or add links in the top toolbar of Anki's main window + + 'links' is a list of HTML link elements. Add-ons can generate their own links + by using aqt.toolbar.Toolbar.create_link. Links created in that way can then be + appended to the link list, e.g.: + + def on_top_toolbar_did_init_links(links, toolbar): + my_link = toolbar.create_link(...) + links.append(my_link) + """ + + _hooks: List[Callable[[List[str], "aqt.toolbar.Toolbar"], None]] = [] + + def append(self, cb: Callable[[List[str], "aqt.toolbar.Toolbar"], None]) -> None: + """(links: List[str], top_toolbar: aqt.toolbar.Toolbar)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[List[str], "aqt.toolbar.Toolbar"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, links: List[str], top_toolbar: aqt.toolbar.Toolbar) -> None: + for hook in self._hooks: + try: + hook(links, top_toolbar) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +top_toolbar_did_init_links = _TopToolbarDidInitLinksHook() + + class _UndoStateDidChangeHook: _hooks: List[Callable[[bool], None]] = [] diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 67dca7555..17fc7dfc3 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -4,10 +4,11 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any, Dict, Optional import aqt from anki.lang import _ +from aqt import gui_hooks from aqt.qt import * from aqt.webview import AnkiWebView @@ -28,13 +29,8 @@ class Toolbar: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: self.mw = mw self.web = web - self.link_handlers = { - "decks": self._deckLinkHandler, + self.link_handlers: Dict[str, Callable] = { "study": self._studyLinkHandler, - "add": self._addLinkHandler, - "browse": self._browseLinkHandler, - "stats": self._statsLinkHandler, - "sync": self._syncLinkHandler, } self.web.setFixedHeight(30) self.web.requiresCol = False @@ -58,34 +54,88 @@ class Toolbar: # Available links ###################################################################### + def create_link( + self, + cmd: str, + label: str, + func: Callable, + tip: Optional[str] = None, + id: Optional[str] = None, + ) -> str: + """Generates HTML link element and registers link handler + + Arguments: + cmd {str} -- Command name used for the JS → Python bridge + label {str} -- Display label of the link + func {Callable} -- Callable to be called on clicking the link + + Keyword Arguments: + tip {Optional[str]} -- Optional tooltip text to show on hovering + over the link (default: {None}) + id: {Optional[str]} -- Optional id attribute to supply the link with + (default: {None}) + + Returns: + str -- HTML link element + """ + + self.link_handlers[cmd] = func + + title_attr = f'title="{tip}"' if tip else "" + id_attr = f'id="{id}"' if id else "" + + return ( + f"""""" + f"""{label}""" + ) + def _centerLinks(self): links = [ - ["decks", _("Decks"), _("Shortcut key: %s") % "D"], - ["add", _("Add"), _("Shortcut key: %s") % "A"], - ["browse", _("Browse"), _("Shortcut key: %s") % "B"], - ["stats", _("Stats"), _("Shortcut key: %s") % "T"], + self.create_link( + "decks", + _("Decks"), + self._deckLinkHandler, + tip=_("Shortcut key: %s") % "D", + id="decks", + ), + self.create_link( + "add", + _("Add"), + self._addLinkHandler, + tip=_("Shortcut key: %s") % "A", + id="add", + ), + self.create_link( + "browse", + _("Browse"), + self._browseLinkHandler, + tip=_("Shortcut key: %s") % "B", + id="browse", + ), + self.create_link( + "stats", + _("Stats"), + self._statsLinkHandler, + tip=_("Shortcut key: %s") % "T", + id="stats", + ), ] - return self._linkHTML(links) + self._sync_link() + links.append(self._create_sync_link()) - def _linkHTML(self, links): - buf = "" - for ln, name, title in links: - buf += """ - %s""" % ( - name, - title, - ln, - name, - ) - return buf + gui_hooks.top_toolbar_did_init_links(links, self) - def _sync_link(self) -> str: + return "\n".join(links) + + def _create_sync_link(self) -> str: name = _("Sync") title = _("Shortcut key: %s") % "Y" label = "sync" + self.link_handlers[label] = self._syncLinkHandler + return f""" -{name} +{name} """ diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index c71917407..17e55ea25 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -306,6 +306,20 @@ hooks = [ return_type="str", legacy_hook="setupStyle", ), + Hook( + name="top_toolbar_did_init_links", + args=["links: List[str]", "top_toolbar: aqt.toolbar.Toolbar"], + doc="""Used to modify or add links in the top toolbar of Anki's main window + + 'links' is a list of HTML link elements. Add-ons can generate their own links + by using aqt.toolbar.Toolbar.create_link. Links created in that way can then be + appended to the link list, e.g.: + + def on_top_toolbar_did_init_links(links, toolbar): + my_link = toolbar.create_link(...) + links.append(my_link) + """, + ), Hook( name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"], ),