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"],
),