diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index 779dc4f70..367ad74ba 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -48,3 +48,4 @@ preferences-monthly-backups = Monthly backups to keep: preferences-minutes-between-backups = Minutes between automatic backups: preferences-reduce-motion = Reduce motion preferences-reduce-motion-tooltip = Disable various animations and transitions of the user interface +preferences-collapse-toolbar = Hide top bar during review diff --git a/qt/aqt/data/web/css/deckbrowser.scss b/qt/aqt/data/web/css/deckbrowser.scss index 7bfac78dc..00e875290 100644 --- a/qt/aqt/data/web/css/deckbrowser.scss +++ b/qt/aqt/data/web/css/deckbrowser.scss @@ -15,7 +15,9 @@ table { &:hover { @include elevation(2); } - transition: box-shadow 0.2s ease-in-out; + transition: box-shadow var(--transition) ease-in-out; + background: var(--canvas-glass); + backdrop-filter: blur(var(--blur)); } a.deck { diff --git a/qt/aqt/data/web/css/reviewer-bottom.scss b/qt/aqt/data/web/css/reviewer-bottom.scss index 0e97d1b43..2c1eb0dad 100644 --- a/qt/aqt/data/web/css/reviewer-bottom.scss +++ b/qt/aqt/data/web/css/reviewer-bottom.scss @@ -18,10 +18,6 @@ body { padding: 0; } -#innertable { - padding-top: 10px; -} - #middle td[align="center"] { padding-top: 10px; position: relative; @@ -30,7 +26,7 @@ body { button { min-width: 60px; white-space: nowrap; - margin: 0.5em; + margin: 9px; position: relative; } @@ -51,10 +47,6 @@ button { font-weight: normal; } -#ansbut { - margin-bottom: 1em; -} - :focus { border-color: color(border-focus); } diff --git a/qt/aqt/data/web/css/toolbar-bottom.scss b/qt/aqt/data/web/css/toolbar-bottom.scss index 5eb082dde..28e0f1f41 100644 --- a/qt/aqt/data/web/css/toolbar-bottom.scss +++ b/qt/aqt/data/web/css/toolbar-bottom.scss @@ -3,7 +3,6 @@ #header { border-bottom: 0; - margin-bottom: 6px; margin-top: 0; padding: 9px; } diff --git a/qt/aqt/data/web/css/toolbar.scss b/qt/aqt/data/web/css/toolbar.scss index 76b0c74f3..441049dc2 100644 --- a/qt/aqt/data/web/css/toolbar.scss +++ b/qt/aqt/data/web/css/toolbar.scss @@ -6,25 +6,27 @@ @use "sass/elevation" as *; @use "sass/button-mixins" as button; -#header { - padding-bottom: 4px; - margin-top: -3px; -} - -.tdcenter { +.toolbar { + display: inline-block; white-space: nowrap; - border-radius: prop(border-radius); + overflow: hidden; border-bottom-left-radius: prop(border-radius-large); border-bottom-right-radius: prop(border-radius-large); - @include button.base($with-hover: false, $with-active: false); - overflow: hidden; - padding: 0; + @include elevation(1, $opacity-boost: -0.1); - @include elevation(1, $opacity-boost: -0.08); - &:hover { - @include elevation(2); + // elevated state (deck browser, overview) + body:not(.flat) & { + background: var(--canvas-elevated); + @include elevation(1); + &:hover { + @include elevation(2); + } } - transition: box-shadow 0.2s ease-in-out; + // glass effect + background: var(--canvas-glass);backdrop-filter: unset; + backdrop-filter: blur(var(--blur)); + + transition: all var(--transition) ease-in-out; } body { @@ -32,6 +34,11 @@ body { padding: 0; -webkit-user-select: none; overflow: hidden; + + &.collapsed { + transform: translateY(-100vh); + } + transition: transform var(--transition) ease-in-out; } * { @@ -40,12 +47,16 @@ body { .hitem { font-weight: bold; - padding: 8px 14px; + padding: 6px 12px 8px; text-decoration: none; color: color(fg); display: inline-block; - @include button.base; - border: none; + + body:not(.flat) &, + &:hover { + @include button.base($border: false); + background: var(--canvas-elevated); + } &:first-child { padding-left: 18px; } @@ -75,7 +86,7 @@ body { display: inline-block; visibility: visible !important; animation-timing-function: linear; - transition: all 0.2s ease-in; + transition: all var(--transition) ease-in; } #sync-spinner { diff --git a/qt/aqt/data/web/css/webview.scss b/qt/aqt/data/web/css/webview.scss index 3f97e21e8..dd954efee 100644 --- a/qt/aqt/data/web/css/webview.scss +++ b/qt/aqt/data/web/css/webview.scss @@ -15,13 +15,21 @@ body { color: var(--fg); background: var(--canvas); - transition: opacity 0.5s ease-out; + transition: opacity var(--transition-medium) ease-out; margin: 2em; overscroll-behavior: none; &:not(.isMac), &:not(.isMac) * { @include scrollbar.custom; } + &.reduced-motion, + &.reduced-motion * { + transition: none !important; + animation: none !important; + } + &.no-blur * { + backdrop-filter: none !important; + } } a { diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index af8c46fff..c550fd45e 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -7,7 +7,7 @@ 0 0 640 - 640 + 660 @@ -113,6 +113,13 @@ + + + + preferences_collapse_toolbar + + + @@ -666,7 +673,7 @@ - + preferences_some_settings_will_take_effect_after @@ -696,6 +703,7 @@ ignore_accents_in_search legacy_import_export reduce_motion + collapse_toolbar useCurrent default_search_text uiScale diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 296e8264c..1e98a6ec8 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -66,6 +66,7 @@ from aqt.qt import sip from aqt.sync import sync_collection, sync_login from aqt.taskman import TaskManager from aqt.theme import Theme, theme_manager +from aqt.toolbar import Toolbar, ToolbarWebView from aqt.undo import UndoActionsInfo from aqt.utils import ( HelpPage, @@ -143,6 +144,27 @@ class MainWebView(AnkiWebView): # currently safe for us to import more than one file at once return + # Main webview specific event handling + def eventFilter(self, obj, evt): + if handled := super().eventFilter(obj, evt): + return handled + + if evt.type() == QEvent.Type.Leave: + if self.mw.pm.collapse_toolbar(): + # Expand toolbar when mouse moves above main webview + # and automatically collapse it with delay after mouse leaves + if self.mapFromGlobal(QCursor.pos()).y() < self.geometry().y(): + if self.mw.toolbarWeb.collapsed: + self.mw.toolbarWeb.expand() + return True + + if evt.type() == QEvent.Type.Enter: + if self.mw.pm.collapse_toolbar(): + self.mw.toolbarWeb.hide_timer.start() + return True + + return False + class AnkiQt(QMainWindow): col: Collection @@ -707,10 +729,16 @@ class AnkiQt(QMainWindow): def _reviewState(self, oldState: MainWindowState) -> None: self.reviewer.show() + if self.pm.collapse_toolbar(): + self.toolbarWeb.collapse() + else: + self.toolbarWeb.flatten() def _reviewCleanup(self, newState: MainWindowState) -> None: if newState != "resetRequired" and newState != "review": self.reviewer.cleanup() + self.toolbarWeb.elevate() + self.toolbarWeb.expand() # Resetting state ########################################################################## @@ -844,10 +872,8 @@ title="{}" {}>{}""".format( self.form = aqt.forms.main.Ui_MainWindow() self.form.setupUi(self) # toolbar - tweb = self.toolbarWeb = AnkiWebView(title="top toolbar") - tweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus) - tweb.disable_zoom() - self.toolbar = aqt.toolbar.Toolbar(self, tweb) + tweb = self.toolbarWeb = ToolbarWebView(self, title="top toolbar") + self.toolbar = Toolbar(self, tweb) # main area self.web = MainWebView(self) # bottom area @@ -1332,6 +1358,10 @@ title="{}" {}>{}""".format( window.windowState() ^ Qt.WindowState.WindowFullScreen ) + def collapse_toolbar_if_allowed(self) -> None: + if self.pm.collapse_toolbar() and self.state == "review": + self.toolbarWeb.collapse() + # Auto update ########################################################################## diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 9f485764b..0b9048ff6 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -208,6 +208,7 @@ class Preferences(QDialog): def setup_global(self) -> None: "Setup options global to all profiles." self.form.reduce_motion.setChecked(self.mw.pm.reduced_motion()) + self.form.collapse_toolbar.setChecked(self.mw.pm.collapse_toolbar()) self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100)) themes = [ tr.preferences_theme_label(theme=theme) @@ -238,7 +239,7 @@ class Preferences(QDialog): restart_required = True self.mw.pm.set_reduced_motion(self.form.reduce_motion.isChecked()) - + self.mw.pm.set_collapse_toolbar(self.form.collapse_toolbar.isChecked()) self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked()) if restart_required: diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 1b377894f..b7277057a 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -524,6 +524,12 @@ create table if not exists profiles def set_reduced_motion(self, on: bool) -> None: self.meta["reduced_motion"] = on + def collapse_toolbar(self) -> bool: + return self.meta.get("collapse_toolbar", False) + + def set_collapse_toolbar(self, on: bool) -> None: + self.meta["collapse_toolbar"] = on + def last_addon_update_check(self) -> int: return self.meta.get("last_addon_update_check", 0) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 5c300ae3f..dda339e93 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -543,6 +543,8 @@ class Reviewer: self.showContextMenu() elif url.startswith("play:"): play_clicked_audio(url, self.card) + elif url.startswith("updateToolbar"): + self.mw.toolbarWeb.update_background_image() else: print("unrecognized anki link:", url) diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index d846ad6fb..a68e53860 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from typing import cast -from aqt import colors +from aqt import colors, props from aqt.qt import * from aqt.theme import theme_manager @@ -173,7 +173,7 @@ class Switch(QAbstractButton): def _animate_toggle(self) -> None: animation = QPropertyAnimation(self, cast(QByteArray, b"position"), self) - animation.setDuration(100) + animation.setDuration(int(theme_manager.var(props.TRANSITION))) animation.setStartValue(self.start_position) animation.setEndValue(self.end_position) # hide label during animation diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 20df25cef..48ac93f6d 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -25,6 +25,7 @@ from aqt.qt import ( QStyleFactory, Qt, qtmajor, + qtminor, ) @@ -169,6 +170,8 @@ class ThemeManager: classes.append("macos-dark-mode") if aqt.mw.pm.reduced_motion(): classes.append("reduced-motion") + if qtmajor == 5 and qtminor < 15: + classes.append("no-blur") return " ".join(classes) def body_classes_for_card_ord( diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index cae262e5b..9b0bcfd64 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -2,7 +2,8 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations -from typing import Any +import re +from typing import Any, Optional import aqt from anki.sync import SyncStatus @@ -25,6 +26,76 @@ class BottomToolbar: self.toolbar = toolbar +class ToolbarWebView(AnkiWebView): + def __init__(self, mw: aqt.AnkiQt, title: str) -> None: + AnkiWebView.__init__(self, mw, title=title) + self.mw = mw + self.setFocusPolicy(Qt.FocusPolicy.WheelFocus) + self.disable_zoom() + self.collapsed = False + self.web_height = 0 + # collapse timer + self.hide_timer = QTimer() + self.hide_timer.setSingleShot(True) + self.hide_timer.setInterval(1000) + qconnect(self.hide_timer.timeout, self.mw.collapse_toolbar_if_allowed) + + def eventFilter(self, obj, evt): + if handled := super().eventFilter(obj, evt): + return handled + + # prevent collapse if pointer inside + if evt.type() == QEvent.Type.Enter: + self.hide_timer.stop() + self.hide_timer.setInterval(1000) + return True + + return False + + def _onHeight(self, qvar: Optional[int]) -> None: + super()._onHeight(qvar) + self.web_height = int(qvar) + + def collapse(self) -> None: + self.collapsed = True + self.eval("""document.body.classList.add("collapsed"); """) + + def expand(self) -> None: + self.collapsed = False + self.eval("""document.body.classList.remove("collapsed"); """) + + def flatten(self) -> None: + self.eval("document.body.classList.add('flat'); ") + + def elevate(self) -> None: + self.eval( + """ + document.body.classList.remove("flat"); + document.body.style.removeProperty("background"); + """ + ) + + def update_background_image(self) -> None: + def set_background(val: str) -> None: + # remove offset from copy + background = re.sub(r"-\d+px ", "0%", val) + # change computedStyle px value back to 100vw + background = re.sub(r"\d+px", "100vw", background) + + self.eval( + f"""document.body.style.setProperty("background", '{background}'); """ + ) + # offset reviewer background by toolbar height + self.mw.web.eval( + f"""document.body.style.setProperty("background-position-y", "-{self.web_height}px"); """ + ) + + self.mw.web.evalWithCallback( + """window.getComputedStyle(document.body).background; """, + set_background, + ) + + class Toolbar: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: self.mw = mw @@ -32,7 +103,6 @@ class Toolbar: self.link_handlers: dict[str, Callable] = { "study": self._studyLinkHandler, } - self.web.setFixedHeight(30) self.web.requiresCol = False def draw( @@ -195,11 +265,10 @@ class Toolbar: ###################################################################### _body = """ -
- - - - +
+
""" diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 5bb7de371..5bba81fc7 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -634,7 +634,6 @@ html {{ {font} }} from aqt import mw if qvar is None: - mw.progress.single_shot(1000, mw.reset) return diff --git a/qt/tools/extract_sass_vars.py b/qt/tools/extract_sass_vars.py index aff1031fb..bc2422596 100644 --- a/qt/tools/extract_sass_vars.py +++ b/qt/tools/extract_sass_vars.py @@ -47,10 +47,14 @@ for line in re.split(r"[;\{\}]|\*\/", data): print("failed to match", line) continue + # convert variable names to Qt style var = m.group(1).replace("-", "_").upper() val = m.group(2) if reached_props: + # remove trailing ms from time props + val = re.sub(r"^(\d+)ms$", r"\1", val) + if not var in props: props.setdefault(var, {})["comment"] = comment props[var]["light"] = val diff --git a/sass/_vars.scss b/sass/_vars.scss index fe7a08ac9..ef10bca04 100644 --- a/sass/_vars.scss +++ b/sass/_vars.scss @@ -32,6 +32,34 @@ $vars: ( ), ), ), + transition: ( + default: ( + "Default duration of transitions in milliseconds", + ( + default: 180ms, + ), + ), + medium: ( + "Slightly longer transition duration in milliseconds", + ( + default: 500ms, + ), + ), + slow: ( + "Long transition duration in milliseconds", + ( + default: 1000ms, + ), + ), + ), + blur: ( + default: ( + "Default background blur value", + ( + default: 20px, + ) + ) + ) ), colors: ( fg: ( @@ -107,6 +135,13 @@ $vars: ( dark: palette(darkgray, 6), ), ), + glass: ( + "Transparent background for surfaces containing text", + ( + light: color.scale(white, $alpha: -60%), + dark: color.scale(palette(darkgray, 4), $alpha: -60%), + ), + ), ), border: ( default: ( diff --git a/sass/base.scss b/sass/base.scss index 0491a59fb..fd8b0e148 100644 --- a/sass/base.scss +++ b/sass/base.scss @@ -45,7 +45,7 @@ html { button { /* override transition for instant hover response */ - transition: color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !important; + transition: color var(--transition) ease-in-out, box-shadow var(--transition) ease-in-out !important; border-radius: prop(border-radius); @include button.base; } diff --git a/sass/buttons.scss b/sass/buttons.scss index 85b93eace..215df2cce 100644 --- a/sass/buttons.scss +++ b/sass/buttons.scss @@ -33,6 +33,6 @@ button { @include elevation(1, $opacity-boost: -0.08); &:hover { @include elevation(2); - transition: box-shadow 0.2s linear; + transition: box-shadow var(--transition) linear; } } diff --git a/sass/core.scss b/sass/core.scss index d6562ed17..46bfb6fc8 100644 --- a/sass/core.scss +++ b/sass/core.scss @@ -11,7 +11,7 @@ body { color: var(--fg); background: var(--canvas); margin: 1em; - transition: opacity 0.5s ease-out; + transition: opacity var(--transition-medium) ease-out; overscroll-behavior: none; } diff --git a/ts/components/PaneContent.svelte b/ts/components/PaneContent.svelte index dedb19d1b..516e4a85b 100644 --- a/ts/components/PaneContent.svelte +++ b/ts/components/PaneContent.svelte @@ -75,7 +75,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html z-index: 4; height: var(--client-height); box-shadow: var(--box-shadow); - transition: box-shadow 0.1s ease-in-out; + transition: box-shadow var(--transition) ease-in-out; } } diff --git a/ts/components/TitledContainer.svelte b/ts/components/TitledContainer.svelte index e65f1441f..e653c1caf 100644 --- a/ts/components/TitledContainer.svelte +++ b/ts/components/TitledContainer.svelte @@ -63,7 +63,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html @include elevation(4); } } - transition: box-shadow 0.2s ease-in-out; + transition: box-shadow var(--transition) ease-in-out; page-break-inside: avoid; } h1 { @@ -73,7 +73,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html right: 0; bottom: 4px; color: var(--fg-faint); - transition: color 0.2s linear; + transition: color var(--transition) linear; &:hover { transition: none; color: var(--fg); diff --git a/ts/deck-options/deck-options-base.scss b/ts/deck-options/deck-options-base.scss index b88d78d93..9710faa22 100644 --- a/ts/deck-options/deck-options-base.scss +++ b/ts/deck-options/deck-options-base.scss @@ -1,7 +1,7 @@ @import "sass/base"; // override Bootstrap transition duration -$carousel-transition: 0.2s; +$carousel-transition: var(--transition); @import "bootstrap/scss/buttons"; @import "bootstrap/scss/button-group"; diff --git a/ts/editor/CollapseBadge.svelte b/ts/editor/CollapseBadge.svelte index 608e1a1cc..3ca97677c 100644 --- a/ts/editor/CollapseBadge.svelte +++ b/ts/editor/CollapseBadge.svelte @@ -18,7 +18,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html .collapse-badge { display: inline-block; opacity: 0.4; - transition: opacity 0.2s ease-in-out, transform 80ms ease-in; + transition: opacity var(--transition) ease-in-out, + transform var(--transition) ease-in; &.highlighted { opacity: 1; } diff --git a/ts/graphs/CardCounts.svelte b/ts/graphs/CardCounts.svelte index 0ffcfe02e..67876129d 100644 --- a/ts/graphs/CardCounts.svelte +++ b/ts/graphs/CardCounts.svelte @@ -87,7 +87,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html