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 = """
-
-
+
+