From 91d563278f201ade223185a203d549153af1d753 Mon Sep 17 00:00:00 2001 From: Aristotelis Date: Mon, 9 Jan 2023 23:48:50 +0100 Subject: [PATCH] Fix toolbar add-on breakages and introduce toolbar tray layout & API (#2301) * Layout toolbar using CSS grid, introducing left and right trays The trays provide a space for add-ons to introduce their own widgets to the toolbar without interfering with each other. * Align tray items to the top * Move absolutely positioned add-on items to right toolbar tray Workaround that fixes breakages in add-ons like AMBOSS, Study Timer, and potentially others that currently still inject absolutely positioned elements into the toolbar using `top_toolbar_did_init_links`. * Account for add-ons that add manual padding (e.g. Study Timer) * Add docstrings and slightly refactor * Tweak item alignment * Introduce hooks for extending left and right toolbar trays * Assign CSS classes to all tray items * Add disclaimer on transitional nature of new hooks --- qt/aqt/data/web/css/toolbar.scss | 27 +++++++++++++- qt/aqt/data/web/js/toolbar.ts | 60 ++++++++++++++++++++++++++++++++ qt/aqt/toolbar.py | 31 ++++++++++++++--- qt/tools/genhooks_gui.py | 30 ++++++++++++++++ 4 files changed, 142 insertions(+), 6 deletions(-) diff --git a/qt/aqt/data/web/css/toolbar.scss b/qt/aqt/data/web/css/toolbar.scss index 441049dc2..15dca6fb0 100644 --- a/qt/aqt/data/web/css/toolbar.scss +++ b/qt/aqt/data/web/css/toolbar.scss @@ -6,8 +6,33 @@ @use "sass/elevation" as *; @use "sass/button-mixins" as button; +.header { + height: 41px; + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: start; + align-content: space-between; +} + +.left-tray { + justify-self: start; +} + +.right-tray { + justify-self: end; +} + +.left-tray, +.right-tray { + align-self: start; + display: flex; + flex-direction: row; + align-items: start; +} + .toolbar { - display: inline-block; + height: 31px; + justify-self: center; white-space: nowrap; overflow: hidden; border-bottom-left-radius: prop(border-radius-large); diff --git a/qt/aqt/data/web/js/toolbar.ts b/qt/aqt/data/web/js/toolbar.ts index b8e4f6be8..e01c67920 100644 --- a/qt/aqt/data/web/js/toolbar.ts +++ b/qt/aqt/data/web/js/toolbar.ts @@ -27,3 +27,63 @@ function updateSyncColor(state: SyncState) { break; } } + +// Dealing with legacy add-ons that used CSS to absolutely position +// themselves at toolbar edges + +function isAbsolutelyPositioned(node: Node): boolean { + if (!(node instanceof HTMLElement)) { + return false; + } + return getComputedStyle(node).position === "absolute"; +} + +function isLegacyAddonElement(node: Node): boolean { + if (isAbsolutelyPositioned(node)) { + return true; + } + for (const child of node.childNodes) { + if (isAbsolutelyPositioned(child)) { + return true; + } + } + return false; +} + +function getElementDimensions(element: HTMLElement): [number, number] { + const widths = [element.offsetWidth]; + const heights = [element.offsetHeight]; + // Some add-ons inject spans or anchors into the toolbar whose dimensions, + // as reported by the properties above are zero, but still occupy space due + // to their child elements: + for (const child of element.childNodes) { + if (!(child instanceof HTMLElement)) { + continue; + } + widths.push(child.offsetWidth); + heights.push(child.offsetHeight); + } + return [Math.max(...widths), Math.max(...heights)]; +} + +function moveLegacyAddonsToTray() { + const rightTray = document.getElementsByClassName("right-tray")[0]; + const toolbarChildren = document.querySelectorAll(".toolbar > *"); + const legacyAddonElements: HTMLElement[] = Array.from(toolbarChildren) + .reverse() // restore original add-on load order + .filter(isLegacyAddonElement); + + for (const element of legacyAddonElements) { + const wrapperElement = document.createElement("div"); + const dimensions = getElementDimensions(element); + element.style.right = "0px"; // remove manual padding + wrapperElement.append(element); + wrapperElement.style.cssText = `\ +width: ${dimensions[0]}px; height: ${dimensions[1]}}px; +margin-left: 5px; margin-right: 5px; position: relative;`; + wrapperElement.className = "tray-item tray-item-legacy"; + rightTray.append(wrapperElement); + } +} + +document.addEventListener("DOMContentLoaded", moveLegacyAddonsToTray); diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 9b0bcfd64..6e1bbd2b5 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -114,8 +114,13 @@ class Toolbar: web_context = web_context or TopToolbar(self) link_handler = link_handler or self._linkHandler self.web.set_bridge_command(link_handler, web_context) + body = self._body.format( + toolbar_content=self._centerLinks(), + left_tray_content=self._left_tray_content(), + right_tray_content=self._right_tray_content(), + ) self.web.stdHtml( - self._body % self._centerLinks(), + body, css=["css/toolbar.css"], js=["js/vendor/jquery.min.js", "js/toolbar.js"], context=web_context, @@ -204,6 +209,22 @@ class Toolbar: return "\n".join(links) + # Add-ons + ###################################################################### + + def _left_tray_content(self) -> str: + left_tray_content: list[str] = [] + gui_hooks.top_toolbar_will_set_left_tray_content(left_tray_content, self) + return self._process_tray_content(left_tray_content) + + def _right_tray_content(self) -> str: + right_tray_content: list[str] = [] + gui_hooks.top_toolbar_will_set_right_tray_content(right_tray_content, self) + return self._process_tray_content(right_tray_content) + + def _process_tray_content(self, content: list[str]) -> str: + return "\n".join(f"""
{item}
""" for item in content) + # Sync ###################################################################### @@ -265,11 +286,11 @@ class Toolbar: ###################################################################### _body = """ -
-
""" diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 145de2cb0..f4fda4d17 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -795,6 +795,36 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest) links.append(my_link) """, ), + Hook( + name="top_toolbar_will_set_left_tray_content", + args=["content: list[str]", "top_toolbar: aqt.toolbar.Toolbar"], + doc="""Used to add custom add-on components to the *left* area of Anki's main + window toolbar + + 'content' is a list of HTML strings added by add-ons which you can append your + own components or elements to. To equip your components with logic and styling + please see `webview_will_set_content` and `webview_did_receive_js_message`. + + Please note that Anki's main screen is due to undergo a significant refactor + in the future and, as a result, add-ons subscribing to this hook will likely + require changes to continue working. + """, + ), + Hook( + name="top_toolbar_will_set_right_tray_content", + args=["content: list[str]", "top_toolbar: aqt.toolbar.Toolbar"], + doc="""Used to add custom add-on components to the *right* area of Anki's main + window toolbar + + 'content' is a list of HTML strings added by add-ons which you can append your + own components or elements to. To equip your components with logic and styling + please see `webview_will_set_content` and `webview_did_receive_js_message`. + + Please note that Anki's main screen is due to undergo a significant refactor + in the future and, as a result, add-ons subscribing to this hook will likely + require changes to continue working. + """, + ), Hook( name="top_toolbar_did_redraw", args=["top_toolbar: aqt.toolbar.Toolbar"],