Auto-hide toolbar in Reviewer (#2262)

* Give webviews a slide-in animation

if reduced motion isn't set.

* Auto-hide toolbar in review mode

moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s.

* Save some space on bottom toolbars

* Use props for all hard-coded transition durations

and decrease most commonly used duration (200ms) to 150ms.

* Move auto-hide logic into ToolbarWebView

and handle auto-hide specific events in the respective webview subclasses.

* Fix typing issues

* Fix flickering issue

* Add auto_hide_toolbar opt-in to preferences

* Rename hide_toolbar to collapse_toolbar

to better describe the dock-like behaviour.

* Rename setting to minimize_distractions

* Reduce calls to pm in eventFilter

* Run formatter

* Revert setting title to something more specific

* Increase default animation time to 180ms

* Inset toolbar in review mode

when auto-hide is not enabled.

* Use card background on toolbar and add glass effect

* Use flatten/elevate over inset/outset

* Use flatten/elevate over inset/outset

* Update toolbar.py

* Fix toolbar background delay

* Tweak styles

* Use "collapse" instead of "auto-hide"

* Fix background misalignment in collapse mode

* Do not collapse toolbar when pointer is outside MainWebView

* Reduce hide_timer interval to 1000ms

* Use CSS to hide toolbar instead of setting webview height

* Add guard to prevent backdrop-filter: blur on Qt 5.14

* Apply transition to body instead of toolbar

to not complicate things for #2301.

* Fix Qt 5.14 and apply guard globally

* Fix background image scaling difference

* Tweak preference wording (dae)
This commit is contained in:
Matthias Metelka 2023-01-09 05:39:31 +01:00 committed by GitHub
parent afca2f52ce
commit 9f8667fb47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 236 additions and 59 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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);
}

View file

@ -3,7 +3,6 @@
#header {
border-bottom: 0;
margin-bottom: 6px;
margin-top: 0;
padding: 9px;
}

View file

@ -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 {

View file

@ -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 {

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>640</width>
<height>640</height>
<height>660</height>
</rect>
</property>
<property name="windowTitle">
@ -113,6 +113,13 @@
</property>
</widget>
</item>
<item alignment="Qt::AlignLeft">
<widget class="QCheckBox" name="collapse_toolbar">
<property name="text">
<string>preferences_collapse_toolbar</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="useCurrent">
<item>
@ -666,7 +673,7 @@
</widget>
</widget>
</item>
<item alignment="Qt::AlignCenter">
<item>
<widget class="QLabel" name="label_21">
<property name="text">
<string>preferences_some_settings_will_take_effect_after</string>
@ -696,6 +703,7 @@
<tabstop>ignore_accents_in_search</tabstop>
<tabstop>legacy_import_export</tabstop>
<tabstop>reduce_motion</tabstop>
<tabstop>collapse_toolbar</tabstop>
<tabstop>useCurrent</tabstop>
<tabstop>default_search_text</tabstop>
<tabstop>uiScale</tabstop>

View file

@ -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="{}" {}>{}</button>""".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="{}" {}>{}</button>""".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
##########################################################################

View file

@ -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:

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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(

View file

@ -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 = """
<center id=outer>
<table id=header>
<tr>
<td class=tdcenter align=center>%s</td>
</tr></table>
<center id="outer">
<div id="header">
<div class="toolbar">%s<div>
</div>
</center>
"""

View file

@ -634,7 +634,6 @@ html {{ {font} }}
from aqt import mw
if qvar is None:
mw.progress.single_shot(1000, mw.reset)
return

View file

@ -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

View file

@ -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: (

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}
</style>

View file

@ -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);

View file

@ -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";

View file

@ -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;
}

View file

@ -87,7 +87,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss">
svg {
transition: opacity 1s;
transition: opacity var(--transition-slow);
}
.counts-outer {

View file

@ -148,7 +148,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
&.loading {
opacity: 0.5;
transition: opacity 1s;
transition: opacity var(--transition-slow);
}
}

View file

@ -50,7 +50,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
font-size: 15px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
transition: opacity var(--transition);
color: var(--fg);
background: var(--canvas-overlay);

View file

@ -145,6 +145,9 @@ export async function _updateQA(
await _runHook(onUpdateHook);
// dynamic toolbar background
bridgeCommand("updateToolbar");
// wait for mathjax to ready
await MathJax.startup.promise
.then(() => {

View file

@ -10,7 +10,10 @@ hr {
body {
margin: 20px;
overflow-wrap: break-word;
background-color: var(--canvas);
// default background setting to fit with toolbar
background-size: 100vw;
background-repeat: no-repeat;
background-attachment: fixed;
}
// explicit nightMode definition required