Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289)

* Create widget gallery dialog

* Add WidgetGallery to debug dialog

* Use enum for its intended purpose

* Rename "reduced-motion" to "reduce-motion"

* Add another border-radius value

and make former large radius a bit smaller.

* Revamp preferences, add minimalist mode

Also:
- create additional and missing widget styles and tweak existing ones
- use single profile entry to set widget styles and reduce choices to Anki and Native

* Indent QTabBar style definitions

* Add missing styles for QPushButton states

* Fix QTableView background

* Remove unused layout from Preferences

* Fix QTabView focused tab style

* Highlight QCheckBox and QRadioButton when focused

* Fix toolbar styles

* Reorder preferences

* Add setting to hide bottom toolbar

* Move toolbar settings above minimalist modes

* Remove unused lines

* Implement proper full-screen mode

* Sort imports

* Tweak deck overview appearance in minimalist mode

* Undo TitledContainer changes

since nobody asked for that

* Remove dynamic toolbar background from minimalist mode

* Tweak buttons in minimalist mode

* Fix some issues

* Reduce theme check interval to 5s on Linux

* Increase hide timer interval to 2s

* Collapse toolbars with slight delay when moving to review state

This should ensure the bottom toolbar collapses too.

* Allow users to make hiding exclusive to full screen

* Rename full screen option

* Fix hide mode dropdown ignoring checkbox state on startup

* Fix typing issue

* Refine background image handling

Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same.

To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand.

* Fix top toolbar getting huge when switching modes

The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight.

* Prevent scrollbar from appearing on bottom toolbar resize

* Cleanup

* Put review tab before editing; fix some tab orders

* Rename 'network' to 'syncing'

* Fix bottom toolbar disappearing on UI > 100

* Improve Preferences layout by adding vertical spacers to the bottom

also make the hiding of video_driver and its label more obvious in preferences.py.

* Fix bottom toolbar animating on startup

Also fix bottom toolbar not appearing when unchecking hide mode in reviewer.

* Hide/Show menubar in fullscreen mode along with toolbar

* Attempt to fix broken native theme on macOS

* Format

* Improve native theme on other systems by not forcing palette

with the caveat that theme switching can get weird.

* Fix theme switching in native style

* Remove redundant condition

* Add back check for Qt5 to prevent theme issues

* Add check for macOS before setting fusion theme

* Do not force scrollbar styles on macOS

* Remove all of that crazy theme logic

* Use canvas instead of button-bg for ColorRole.Button

* Make sure Anki style is always based on Fusion

otherwise we can't guarantee the same look on all systems.

* Explicitly apply default style when Anki style is not selected

This should fix the style not switching back after it was selected.

* Remove reduncant default_palette

* Revert 8af4c1cc2

On Mac with native theme, both Qt5 and Qt6 look correct already. On
the Anki theme, without this change, we get the fusion-style scrollbars
instead of the rounded ones.

* Rename AnkiStyles enum to WidgetStyle

* Fix theme switching shades on same theme

* Format

* Remove unused placeholderText

that caused an error when opening the widget gallery on Qt5.

* Check for full screen windowState using bitwise operator

to prevent error in Qt5.

Credit: https://stackoverflow.com/a/65425151

* Hide style option on Windows

also exclude native option from dropdown just in case.

* Format

* Minor naming tweak
This commit is contained in:
Matthias Metelka 2023-01-18 12:24:16 +01:00 committed by GitHub
parent c923553a53
commit f169ee0933
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 2495 additions and 1204 deletions

View file

@ -4,13 +4,13 @@ preferences-basic = Basic
preferences-change-deck-depending-on-note-type = Change deck depending on note type preferences-change-deck-depending-on-note-type = Change deck depending on note type
preferences-changes-will-take-effect-when-you = Changes will take effect when you restart Anki. preferences-changes-will-take-effect-when-you = Changes will take effect when you restart Anki.
preferences-hours-past-midnight = hours past midnight preferences-hours-past-midnight = hours past midnight
preferences-interface-language = Interface language: preferences-language = Language
preferences-interrupt-current-audio-when-answering = Interrupt current audio when answering preferences-interrupt-current-audio-when-answering = Interrupt current audio when answering
preferences-learn-ahead-limit = Learn ahead limit preferences-learn-ahead-limit = Learn ahead limit
preferences-mins = mins preferences-mins = mins
preferences-network = Syncing preferences-network = Syncing
preferences-next-day-starts-at = Next day starts at preferences-next-day-starts-at = Next day starts at
preferences-note-media-is-not-backed-up = Note: Media is not backed up. Please create a periodic backup of your Anki folder to be safe. preferences-note-media-is-not-backed-up = Media is not backed up. Please create a periodic backup of your Anki folder to be safe.
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
@ -21,19 +21,19 @@ preferences-scheduling = Scheduling
preferences-show-learning-cards-with-larger-steps = Show learning cards with larger steps before reviews preferences-show-learning-cards-with-larger-steps = Show learning cards with larger steps before reviews
preferences-show-next-review-time-above-answer = Show next review time above answer buttons preferences-show-next-review-time-above-answer = Show next review time above answer buttons
preferences-show-play-buttons-on-cards-with = Show play buttons on cards with audio preferences-show-play-buttons-on-cards-with = Show play buttons on cards with audio
preferences-show-remaining-card-count-during-review = Show remaining card count during review preferences-show-remaining-card-count-during-review = Show remaining card count
preferences-some-settings-will-take-effect-after = Some settings will take effect after you restart Anki. preferences-some-settings-will-take-effect-after = Some settings will take effect after you restart Anki.
preferences-synchronisation = <b>Synchronisation</b> preferences-synchronisation = Synchronisation
preferences-synchronizationnot-currently-enabled-click-the-sync = <b>Synchronization</b><br> Not currently enabled; click the sync button in the main window to enable. preferences-synchronizationnot-currently-enabled-click-the-sync = <b>Synchronization</b><br> Not currently enabled; click the sync button in the main window to enable.
preferences-synchronize-audio-and-images-too = Synchronize audio and images too preferences-synchronize-audio-and-images-too = Synchronize audio and images too
preferences-timebox-time-limit = Timebox time limit preferences-timebox-time-limit = Timebox time limit
preferences-user-interface-size = User interface size preferences-user-interface-size = User interface size
preferences-when-adding-default-to-current-deck = When adding, default to current deck preferences-when-adding-default-to-current-deck = When adding, default to current deck
preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File>Switch Profile. preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File > Switch Profile.
preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14) preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14)
preferences-default-search-text = Default search text preferences-default-search-text = Default search text
preferences-default-search-text-example = eg. 'deck:current ' preferences-default-search-text-example = eg. 'deck:current '
preferences-theme-label = Theme: { $theme } preferences-theme = Theme
preferences-theme-follow-system = Follow System preferences-theme-follow-system = Follow System
preferences-theme-light = Light preferences-theme-light = Light
preferences-theme-dark = Dark preferences-theme-dark = Dark
@ -48,6 +48,25 @@ preferences-monthly-backups = Monthly backups to keep:
preferences-minutes-between-backups = Minutes between automatic backups: preferences-minutes-between-backups = Minutes between automatic backups:
preferences-reduce-motion = Reduce motion preferences-reduce-motion = Reduce motion
preferences-reduce-motion-tooltip = Disable various animations and transitions of the user interface preferences-reduce-motion-tooltip = Disable various animations and transitions of the user interface
preferences-collapse-toolbar = Hide top bar during review
preferences-custom-sync-url = Self-hosted sync server preferences-custom-sync-url = Self-hosted sync server
preferences-custom-sync-url-disclaimer = For advanced users - please see the manual preferences-custom-sync-url-disclaimer = For advanced users - please see the manual
preferences-hide-top-bar-during-review = Hide top bar during review
preferences-hide-bottom-bar-during-review = Hide bottom bar during review
preferences-always = Always
preferences-full-screen-only = Full screen only
preferences-appearance = Appearance
preferences-general = General
preferences-style = Style
preferences-review = Review
preferences-reviewer = Reviewer
preferences-distractions = Distractions
preferences-minimalist-mode = Minimalist mode
preferences-editing = Editing
preferences-browsing = Browsing
preferences-default-deck = Default deck
preferences-account = AnkiWeb Account
preferences-media = Media
preferences-note = Note
preferences-scheduler = Scheduler
preferences-user-interface = User Interface
preferences-import-export = Import/Export

View file

@ -1,6 +1,6 @@
## Video drivers/hardware acceleration. Please avoid translating 'OpenGL' and 'ANGLE'. ## Video drivers/hardware acceleration. Please avoid translating 'OpenGL' and 'ANGLE'.
preferences-video-driver = Video driver: { $driver } preferences-video-driver = Video driver
preferences-video-driver-opengl-mac = OpenGL (recommended on Macs) preferences-video-driver-opengl-mac = OpenGL (recommended on Macs)
preferences-video-driver-software-mac = Software (not recommended) preferences-video-driver-software-mac = Software (not recommended)
preferences-video-driver-opengl-other = OpenGL (faster, may cause issues) preferences-video-driver-opengl-other = OpenGL (faster, may cause issues)

View file

@ -350,6 +350,7 @@ class AnkiApp(QApplication):
QCheckBox, QCheckBox,
QRadioButton, QRadioButton,
QMenu, QMenu,
QSlider,
# classes with PyQt5 compatibility proxy # classes with PyQt5 compatibility proxy
without_qt5_compat_wrapper(QToolButton), without_qt5_compat_wrapper(QToolButton),
without_qt5_compat_wrapper(QTabBar), without_qt5_compat_wrapper(QTabBar),

View file

@ -7,17 +7,19 @@
table { table {
padding: 1rem; padding: 1rem;
background: var(--canvas-elevated);
border: 1px solid var(--border-subtle);
border-radius: var(--border-radius-large);
@include elevation(1, $opacity-boost: -0.08); .fancy & {
&:hover { border: 1px solid var(--border-subtle);
@include elevation(2); border-radius: var(--border-radius-medium);
@include elevation(1, $opacity-boost: -0.08);
&:hover {
@include elevation(2);
}
transition: box-shadow var(--transition) ease-in-out;
background: var(--canvas-glass);
backdrop-filter: blur(var(--blur));
} }
transition: box-shadow var(--transition) ease-in-out;
background: var(--canvas-glass);
backdrop-filter: blur(var(--blur));
} }
a.deck { a.deck {
@ -37,7 +39,13 @@ th {
} }
tr.deck td { tr.deck td {
padding: 4px 12px; padding: 1px 12px;
border-bottom: 1px solid var(--border-subtle);
.fancy & {
border: unset;
padding: 4px 12px;
}
} }
tr.top-level-drag-row td { tr.top-level-drag-row td {
@ -62,12 +70,12 @@ tr:hover:not(.top-level-drag-row) {
td { td {
background: color(border-subtle); background: color(border-subtle);
&:first-child { &:first-child {
border-top-left-radius: prop(border-radius-large); border-top-left-radius: prop(border-radius-medium);
border-bottom-left-radius: prop(border-radius-large); border-bottom-left-radius: prop(border-radius-medium);
} }
&:last-child { &:last-child {
border-top-right-radius: prop(border-radius-large); border-top-right-radius: prop(border-radius-medium);
border-bottom-right-radius: prop(border-radius-large); border-bottom-right-radius: prop(border-radius-medium);
} }
.gears { .gears {
visibility: visible; visibility: visible;
@ -82,14 +90,14 @@ tr:hover:not(.top-level-drag-row) {
&:first-child { &:first-child {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-top-right-radius: prop(border-radius-large); border-top-right-radius: prop(border-radius-medium);
border-bottom-right-radius: prop(border-radius-large); border-bottom-right-radius: prop(border-radius-medium);
} }
&:last-child { &:last-child {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-top-left-radius: prop(border-radius-large); border-top-left-radius: prop(border-radius-medium);
border-bottom-left-radius: prop(border-radius-large); border-bottom-left-radius: prop(border-radius-medium);
} }
} }
} }

View file

@ -1,6 +1,10 @@
/* Copyright: Ankitects Pty Ltd and contributors /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
body {
overflow: hidden;
}
#header { #header {
border-bottom: 0; border-bottom: 0;
margin-top: 0; margin-top: 0;

View file

@ -7,11 +7,13 @@
@use "sass/button-mixins" as button; @use "sass/button-mixins" as button;
.header { .header {
height: 41px;
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
align-items: start; align-items: start;
align-content: space-between; align-content: space-between;
body:not(.fancy) & {
border-bottom: 1px solid var(--border-subtle);
}
} }
.left-tray { .left-tray {
@ -31,65 +33,89 @@
} }
.toolbar { .toolbar {
height: 31px;
justify-self: center; justify-self: center;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
border-bottom-left-radius: prop(border-radius-large);
border-bottom-right-radius: prop(border-radius-large);
@include elevation(1, $opacity-boost: -0.1);
// elevated state (deck browser, overview)
body:not(.flat) & {
background: var(--canvas-elevated);
@include elevation(1);
&:hover {
@include elevation(2);
}
}
// glass effect
background: var(--canvas-glass);backdrop-filter: unset;
backdrop-filter: blur(var(--blur));
transition: all var(--transition) ease-in-out; transition: all var(--transition) ease-in-out;
} }
.hitem {
font-weight: bold;
padding: 5px 12px;
color: color(fg);
display: inline-block;
&:hover {
text-decoration: underline;
}
}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-user-select: none; -webkit-user-select: none;
overflow: hidden; overflow: hidden;
&.collapsed { &:not(.fancy).hidden {
transform: translateY(-100vh); opacity: 0;
}
transition: opacity var(--transition) ease-in-out;
&.fancy {
margin-bottom: 5px;
&.hidden {
transform: translateY(-100vh);
}
transition: transform var(--transition) ease-in-out;
.toolbar {
overflow: hidden;
border-bottom-left-radius: prop(border-radius-medium);
border-bottom-right-radius: prop(border-radius-medium);
@include elevation(1, $opacity-boost: -0.1);
// glass effect
background: var(--canvas-glass);
backdrop-filter: blur(var(--blur));
}
// elevated state (deck browser, overview)
&:not(.flat) .toolbar {
background: var(--canvas-elevated);
@include elevation(1);
&:hover {
@include elevation(2);
}
}
&:not(.flat) .hitem {
@include button.base($border: false, $with-hover: false);
background: var(--canvas-glass);
border: 1px solid transparent;
}
.hitem {
text-decoration: none;
border: 1px solid transparent;
&:hover {
border: 1px solid var(--border-subtle);
}
&:active {
background: var(--canvas-inset);
}
&:first-child {
padding-left: 18px;
}
&:last-child {
padding-right: 18px;
}
}
} }
transition: transform var(--transition) ease-in-out;
} }
* { * {
-webkit-user-drag: none; -webkit-user-drag: none;
} }
.hitem {
font-weight: bold;
padding: 5px 12px;
text-decoration: none;
color: color(fg);
display: inline-block;
body:not(.flat) &,
&:hover {
@include button.base($border: false);
background: var(--canvas-elevated);
}
&:first-child {
padding-left: 18px;
}
&:last-child {
padding-right: 18px;
}
}
.hitem:focus { .hitem:focus {
outline: 0; outline: 0;
} }

View file

@ -22,14 +22,19 @@ body {
&:not(.isMac) * { &:not(.isMac) * {
@include scrollbar.custom; @include scrollbar.custom;
} }
&.reduced-motion, &.reduce-motion,
&.reduced-motion * { &.reduce-motion * {
transition: none !important; transition: none !important;
animation: none !important; animation: none !important;
} }
&.no-blur * { &.no-blur * {
backdrop-filter: none !important; backdrop-filter: none !important;
} }
&:not(.fancy),
&:not(.fancy) * {
box-shadow: none !important;
backdrop-filter: none !important;
}
} }
a { a {

View file

@ -42,4 +42,5 @@ from . import (
synclog, synclog,
taglimit, taglimit,
template, template,
widgets,
) )

View file

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>643</width> <width>643</width>
<height>580</height> <height>582</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -48,6 +48,9 @@
<property name="lineWrapMode"> <property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum> <enum>QPlainTextEdit::NoWrap</enum>
</property> </property>
<property name="placeholderText">
<string notr="true">Type commands here (Enter to submit)</string>
</property>
</widget> </widget>
<widget class="QPlainTextEdit" name="log"> <widget class="QPlainTextEdit" name="log">
<property name="sizePolicy"> <property name="sizePolicy">
@ -68,9 +71,28 @@
<property name="readOnly"> <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="placeholderText">
<string notr="true">Output</string>
</property>
</widget> </widget>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string notr="true">Styling</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="widgetsButton">
<property name="text">
<string notr="true">Qt Widget Gallery</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@
<item> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>preferences_interface_language</string> <string>preferences_language</string>
</property> </property>
</widget> </widget>
</item> </item>

6
qt/aqt/forms/widgets.py Normal file
View file

@ -0,0 +1,6 @@
from aqt.qt import qtmajor
if qtmajor > 5:
from _aqt.forms.widgets_qt6 import *
else:
from _aqt.forms.widgets_qt5 import * # type: ignore

375
qt/aqt/forms/widgets.ui Normal file
View file

@ -0,0 +1,375 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>925</width>
<height>822</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Qt Widget Gallery</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="topLayout">
<item>
<widget class="QLabel" name="styleLabel">
<property name="text">
<string notr="true">Style</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="styleComboBox">
<property name="currentText">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="disableCheckBox">
<property name="text">
<string notr="true">Disable Widgets</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="testGrid" native="true">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="checkButtonsGroup">
<property name="title">
<string notr="true">Check Buttons</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="radioButtonNotCheckable">
<property name="text">
<string notr="true">RadioButton (not checkable)</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonChecked">
<property name="text">
<string notr="true">RadioButton (checked)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButtonUnchecked">
<property name="text">
<string notr="true">RadioButton (unchecked)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxTristate">
<property name="text">
<string notr="true">CheckBox (tristate)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="buttonsGroup">
<property name="title">
<string notr="true">Buttons</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string notr="true">PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonCheckable">
<property name="text">
<string notr="true">PushButton (checkable)</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonFlat">
<property name="text">
<string notr="true">PushButton (flat)</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="calendarGroup">
<property name="title">
<string notr="true">CalendarWidget</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QCalendarWidget" name="calendarWidget"/>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="textInputsGroup">
<property name="title">
<string notr="true">Text Inputs</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QComboBox" name="comboBox">
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string notr="true">ComboBox (editable)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="text">
<string notr="true"/>
</property>
<property name="placeholderText">
<string notr="true">LineEdit</string>
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QPlainTextEdit" name="plainTextEdit">
<property name="plainText">
<string notr="true"/>
</property>
<property name="placeholderText">
<string notr="true">PlainTextEdit</string>
</property>
</widget>
<widget class="QTextEdit" name="textEdit">
<property name="documentTitle">
<string notr="true"/>
</property>
<property name="placeholderText">
<string notr="true">TextEdit</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="otherInputsGroup">
<property name="title">
<string notr="true">Other Inputs</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1" colspan="2">
<widget class="QKeySequenceEdit" name="keySequenceEdit">
<property name="keySequence">
<string notr="true"/>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QSpinBox" name="spinBox">
<property name="suffix">
<string notr="true"/>
</property>
<property name="prefix">
<string notr="true"/>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="keySequenceLabel">
<property name="text">
<string notr="true">KeySequenceEdit</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="dateTimeLabel">
<property name="text">
<string notr="true">DateTimeEdit</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="spinBoxLabel">
<property name="text">
<string notr="true">SpinBox</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="sliderLabel">
<property name="text">
<string notr="true">Slider</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="horizontalSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Dial</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="dial"/>
</item>
</layout>
</item>
<item row="1" column="1" colspan="2">
<widget class="QDateTimeEdit" name="dateTimeEdit"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="1">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="listTab">
<attribute name="title">
<string notr="true">ListWidget</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QListWidget" name="listWidget"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="treeTab">
<attribute name="title">
<string notr="true">TreeWidget</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QTreeWidget" name="treeWidget">
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tableTab">
<attribute name="title">
<string notr="true">TableWidget</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="progressBarLayout">
<item>
<widget class="QLabel" name="progressBarLabel">
<property name="text">
<string notr="true">ProgressBar</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -66,7 +66,7 @@ from aqt.qt import sip
from aqt.sync import sync_collection, sync_login from aqt.sync import sync_collection, sync_login
from aqt.taskman import TaskManager from aqt.taskman import TaskManager
from aqt.theme import Theme, theme_manager from aqt.theme import Theme, theme_manager
from aqt.toolbar import Toolbar, ToolbarWebView from aqt.toolbar import BottomWebView, Toolbar, TopWebView
from aqt.undo import UndoActionsInfo from aqt.undo import UndoActionsInfo
from aqt.utils import ( from aqt.utils import (
HelpPage, HelpPage,
@ -150,18 +150,17 @@ class MainWebView(AnkiWebView):
return handled return handled
if evt.type() == QEvent.Type.Leave: if evt.type() == QEvent.Type.Leave:
if self.mw.pm.collapse_toolbar(): # Show toolbar when mouse moves outside main webview
# Expand toolbar when mouse moves above main webview # and automatically hide it with delay after mouse has entered again
# and automatically collapse it with delay after mouse leaves if self.mw.pm.hide_top_bar() or self.mw.pm.hide_bottom_bar():
if self.mapFromGlobal(QCursor.pos()).y() < self.geometry().y(): self.mw.toolbarWeb.show()
if self.mw.toolbarWeb.collapsed: self.mw.bottomWeb.show()
self.mw.toolbarWeb.expand()
return True return True
if evt.type() == QEvent.Type.Enter: if evt.type() == QEvent.Type.Enter:
if self.mw.pm.collapse_toolbar(): self.mw.toolbarWeb.hide_timer.start()
self.mw.toolbarWeb.hide_timer.start() self.mw.bottomWeb.hide_timer.start()
return True return True
return False return False
@ -170,7 +169,7 @@ class AnkiQt(QMainWindow):
col: Collection col: Collection
pm: ProfileManagerType pm: ProfileManagerType
web: MainWebView web: MainWebView
bottomWeb: AnkiWebView bottomWeb: BottomWebView
def __init__( def __init__(
self, self,
@ -190,6 +189,7 @@ class AnkiQt(QMainWindow):
aqt.mw = self aqt.mw = self
self.app = app self.app = app
self.pm = profileManager self.pm = profileManager
self.fullscreen = False
# init rest of app # init rest of app
self.safeMode = ( self.safeMode = (
bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier) bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier)
@ -709,7 +709,7 @@ class AnkiQt(QMainWindow):
gui_hooks.state_will_change(state, oldState) gui_hooks.state_will_change(state, oldState)
getattr(self, f"_{state}State", lambda *_: None)(oldState, *args) getattr(self, f"_{state}State", lambda *_: None)(oldState, *args)
if state != "resetRequired": if state != "resetRequired":
self.bottomWeb.show() self.bottomWeb.adjustHeightToFit()
gui_hooks.state_did_change(state, oldState) gui_hooks.state_did_change(state, oldState)
def _deckBrowserState(self, oldState: MainWindowState) -> None: def _deckBrowserState(self, oldState: MainWindowState) -> None:
@ -729,16 +729,23 @@ class AnkiQt(QMainWindow):
def _reviewState(self, oldState: MainWindowState) -> None: def _reviewState(self, oldState: MainWindowState) -> None:
self.reviewer.show() self.reviewer.show()
if self.pm.collapse_toolbar():
self.toolbarWeb.collapse() if self.pm.hide_top_bar():
self.toolbarWeb.hide_timer.setInterval(500)
self.toolbarWeb.hide_timer.start()
else: else:
self.toolbarWeb.flatten() self.toolbarWeb.flatten()
if self.pm.hide_bottom_bar():
self.bottomWeb.hide_timer.setInterval(500)
self.bottomWeb.hide_timer.start()
def _reviewCleanup(self, newState: MainWindowState) -> None: def _reviewCleanup(self, newState: MainWindowState) -> None:
if newState != "resetRequired" and newState != "review": if newState != "resetRequired" and newState != "review":
self.reviewer.cleanup() self.reviewer.cleanup()
self.toolbarWeb.elevate() self.toolbarWeb.elevate()
self.toolbarWeb.expand() self.toolbarWeb.show()
self.bottomWeb.show()
# Resetting state # Resetting state
########################################################################## ##########################################################################
@ -872,12 +879,12 @@ title="{}" {}>{}</button>""".format(
self.form = aqt.forms.main.Ui_MainWindow() self.form = aqt.forms.main.Ui_MainWindow()
self.form.setupUi(self) self.form.setupUi(self)
# toolbar # toolbar
tweb = self.toolbarWeb = ToolbarWebView(self, title="top toolbar") tweb = self.toolbarWeb = TopWebView(self, title="top toolbar")
self.toolbar = Toolbar(self, tweb) self.toolbar = Toolbar(self, tweb)
# main area # main area
self.web = MainWebView(self) self.web = MainWebView(self)
# bottom area # bottom area
sweb = self.bottomWeb = AnkiWebView(title="bottom toolbar") sweb = self.bottomWeb = BottomWebView(self, title="bottom toolbar")
sweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus) sweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
sweb.disable_zoom() sweb.disable_zoom()
# add in a layout # add in a layout
@ -1068,12 +1075,12 @@ title="{}" {}>{}</button>""".format(
if is_lin: if is_lin:
# On Linux, the check requires invoking an external binary, # On Linux, the check requires invoking an external binary,
# which we don't want to be doing frequently # which we don't want to be doing frequently
interval_secs = 300
else:
interval_secs = 5 interval_secs = 5
else:
interval_secs = 2
self.progress.timer( self.progress.timer(
interval_secs * 1000, interval_secs * 1000,
theme_manager.apply_style_if_system_style_changed, theme_manager.apply_style,
True, True,
False, False,
parent=self, parent=self,
@ -1358,9 +1365,24 @@ title="{}" {}>{}</button>""".format(
window.windowState() ^ Qt.WindowState.WindowFullScreen window.windowState() ^ Qt.WindowState.WindowFullScreen
) )
def collapse_toolbar_if_allowed(self) -> None: # Hide Menubar on Windows and Linux
if self.pm.collapse_toolbar() and self.state == "review": if window.windowState() & Qt.WindowState.WindowFullScreen and not is_mac:
self.toolbarWeb.collapse() self.fullscreen = True
self.hide_menubar()
else:
self.fullscreen = False
self.show_menubar()
# Update Toolbar states
self.toolbarWeb.hide_if_allowed()
self.bottomWeb.hide_if_allowed()
def hide_menubar(self) -> None:
self.form.menubar.setFixedHeight(0)
def show_menubar(self) -> None:
self.form.menubar.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
self.form.menubar.setMinimumSize(0, 0)
# Auto update # Auto update
########################################################################## ##########################################################################
@ -1633,6 +1655,8 @@ title="{}" {}>{}</button>""".format(
s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+l"), d) s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+l"), d)
qconnect(s.activated, frm.text.clear) qconnect(s.activated, frm.text.clear)
qconnect(frm.widgetsButton.clicked, self._on_widgetGallery)
def addContextMenu( def addContextMenu(
ev: Union[QCloseEvent, QContextMenuEvent], name: str ev: Union[QCloseEvent, QContextMenuEvent], name: str
) -> None: ) -> None:
@ -1654,6 +1678,12 @@ title="{}" {}>{}</button>""".format(
gui_hooks.debug_console_will_show(d) gui_hooks.debug_console_will_show(d)
d.show() d.show()
def _on_widgetGallery(self) -> None:
from aqt.widgetgallery import WidgetGallery
self.widgetGallery = WidgetGallery(self)
self.widgetGallery.show()
def _captureOutput(self, on: bool) -> None: def _captureOutput(self, on: bool) -> None:
mw2 = self mw2 = self

View file

@ -14,7 +14,15 @@ from aqt.operations.collection import set_preferences
from aqt.profiles import VideoDriver from aqt.profiles import VideoDriver
from aqt.qt import * from aqt.qt import *
from aqt.theme import Theme from aqt.theme import Theme
from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWarning, tr from aqt.utils import (
HelpPage,
disable_help_button,
is_win,
openHelp,
showInfo,
showWarning,
tr,
)
class Preferences(QDialog): class Preferences(QDialog):
@ -209,20 +217,61 @@ class Preferences(QDialog):
def setup_global(self) -> None: def setup_global(self) -> None:
"Setup options global to all profiles." "Setup options global to all profiles."
self.form.reduce_motion.setChecked(self.mw.pm.reduced_motion()) self.form.reduce_motion.setChecked(self.mw.pm.reduce_motion())
self.form.collapse_toolbar.setChecked(self.mw.pm.collapse_toolbar()) qconnect(self.form.reduce_motion.stateChanged, self.mw.pm.set_reduce_motion)
self.form.minimalist_mode.setChecked(self.mw.pm.minimalist_mode())
qconnect(self.form.minimalist_mode.stateChanged, self.mw.pm.set_minimalist_mode)
hide_choices = [tr.preferences_full_screen_only(), tr.preferences_always()]
self.form.hide_top_bar.setChecked(self.mw.pm.hide_top_bar())
qconnect(self.form.hide_top_bar.stateChanged, self.mw.pm.set_hide_top_bar)
qconnect(
self.form.hide_top_bar.stateChanged,
self.form.topBarComboBox.setVisible,
)
self.form.topBarComboBox.addItems(hide_choices)
self.form.topBarComboBox.setCurrentIndex(self.mw.pm.top_bar_hide_mode())
self.form.topBarComboBox.setVisible(self.form.hide_top_bar.isChecked())
qconnect(
self.form.topBarComboBox.currentIndexChanged,
self.mw.pm.set_top_bar_hide_mode,
)
self.form.hide_bottom_bar.setChecked(self.mw.pm.hide_bottom_bar())
qconnect(self.form.hide_bottom_bar.stateChanged, self.mw.pm.set_hide_bottom_bar)
qconnect(
self.form.hide_bottom_bar.stateChanged,
self.form.bottomBarComboBox.setVisible,
)
self.form.bottomBarComboBox.addItems(hide_choices)
self.form.bottomBarComboBox.setCurrentIndex(self.mw.pm.bottom_bar_hide_mode())
self.form.bottomBarComboBox.setVisible(self.form.hide_bottom_bar.isChecked())
qconnect(
self.form.bottomBarComboBox.currentIndexChanged,
self.mw.pm.set_bottom_bar_hide_mode,
)
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100)) self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
themes = [ themes = [
tr.preferences_theme_label(theme=theme) tr.preferences_theme_follow_system(),
for theme in ( tr.preferences_theme_light(),
tr.preferences_theme_follow_system(), tr.preferences_theme_dark(),
tr.preferences_theme_light(),
tr.preferences_theme_dark(),
)
] ]
self.form.theme.addItems(themes) self.form.theme.addItems(themes)
self.form.theme.setCurrentIndex(self.mw.pm.theme().value) self.form.theme.setCurrentIndex(self.mw.pm.theme().value)
qconnect(self.form.theme.currentIndexChanged, self.on_theme_changed) qconnect(self.form.theme.currentIndexChanged, self.on_theme_changed)
self.form.styleComboBox.addItems(["Anki"] + (["Native"] if not is_win else []))
self.form.styleComboBox.setCurrentIndex(self.mw.pm.get_widget_style())
qconnect(
self.form.styleComboBox.currentIndexChanged,
self.mw.pm.set_widget_style,
)
self.form.styleComboBox.setVisible(not is_win)
self.form.legacy_import_export.setChecked(self.mw.pm.legacy_import_export()) self.form.legacy_import_export.setChecked(self.mw.pm.legacy_import_export())
self.setup_language() self.setup_language()
@ -240,8 +289,6 @@ class Preferences(QDialog):
self.mw.pm.setUiScale(newScale) self.mw.pm.setUiScale(newScale)
restart_required = True 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()) self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
if restart_required: if restart_required:
@ -291,15 +338,14 @@ class Preferences(QDialog):
def setup_video_driver(self) -> None: def setup_video_driver(self) -> None:
self.video_drivers = VideoDriver.all_for_platform() self.video_drivers = VideoDriver.all_for_platform()
names = [ names = [video_driver_name_for_platform(d) for d in self.video_drivers]
tr.preferences_video_driver(driver=video_driver_name_for_platform(d))
for d in self.video_drivers
]
self.form.video_driver.addItems(names) self.form.video_driver.addItems(names)
self.form.video_driver.setCurrentIndex( self.form.video_driver.setCurrentIndex(
self.video_drivers.index(self.mw.pm.video_driver()) self.video_drivers.index(self.mw.pm.video_driver())
) )
self.form.video_driver.setVisible(qtmajor == 5) if qtmajor > 5:
self.form.video_driver_label.setVisible(False)
self.form.video_driver.setVisible(False)
def update_video_driver(self) -> None: def update_video_driver(self) -> None:
new_driver = self.video_drivers[self.form.video_driver.currentIndex()] new_driver = self.video_drivers[self.form.video_driver.currentIndex()]

View file

@ -21,9 +21,10 @@ from anki.db import DB
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.sync import SyncAuth from anki.sync import SyncAuth
from anki.utils import int_time, is_mac, is_win, point_version from anki.utils import int_time, is_mac, is_win, point_version
from aqt import appHelpSite from aqt import appHelpSite, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.theme import Theme, theme_manager from aqt.theme import Theme, WidgetStyle, theme_manager
from aqt.toolbar import HideMode
from aqt.utils import disable_help_button, send_to_trash, showWarning, tr from aqt.utils import disable_help_button, send_to_trash, showWarning, tr
if TYPE_CHECKING: if TYPE_CHECKING:
@ -518,17 +519,47 @@ create table if not exists profiles
def setUiScale(self, scale: float) -> None: def setUiScale(self, scale: float) -> None:
self.meta["uiScale"] = scale self.meta["uiScale"] = scale
def reduced_motion(self) -> bool: def reduce_motion(self) -> bool:
return self.meta.get("reduced_motion", False) return self.meta.get("reduce_motion", False)
def set_reduced_motion(self, on: bool) -> None: def set_reduce_motion(self, on: bool) -> None:
self.meta["reduced_motion"] = on self.meta["reduce_motion"] = on
gui_hooks.body_classes_need_update()
def collapse_toolbar(self) -> bool: def minimalist_mode(self) -> bool:
return self.meta.get("collapse_toolbar", False) return self.meta.get("minimalist_mode", False)
def set_collapse_toolbar(self, on: bool) -> None: def set_minimalist_mode(self, on: bool) -> None:
self.meta["collapse_toolbar"] = on self.meta["minimalist_mode"] = on
gui_hooks.body_classes_need_update()
def hide_top_bar(self) -> bool:
return self.meta.get("hide_top_bar", False)
def set_hide_top_bar(self, on: bool) -> None:
self.meta["hide_top_bar"] = on
gui_hooks.body_classes_need_update()
def top_bar_hide_mode(self) -> HideMode:
return self.meta.get("top_bar_hide_mode", HideMode.FULLSCREEN)
def set_top_bar_hide_mode(self, mode: HideMode) -> None:
self.meta["top_bar_hide_mode"] = mode
gui_hooks.body_classes_need_update()
def hide_bottom_bar(self) -> bool:
return self.meta.get("hide_bottom_bar", False)
def set_hide_bottom_bar(self, on: bool) -> None:
self.meta["hide_bottom_bar"] = on
gui_hooks.body_classes_need_update()
def bottom_bar_hide_mode(self) -> HideMode:
return self.meta.get("bottom_bar_hide_mode", HideMode.FULLSCREEN)
def set_bottom_bar_hide_mode(self, mode: HideMode) -> None:
self.meta["bottom_bar_hide_mode"] = mode
gui_hooks.body_classes_need_update()
def last_addon_update_check(self) -> int: def last_addon_update_check(self) -> int:
return self.meta.get("last_addon_update_check", 0) return self.meta.get("last_addon_update_check", 0)
@ -546,11 +577,14 @@ create table if not exists profiles
def set_theme(self, theme: Theme) -> None: def set_theme(self, theme: Theme) -> None:
self.meta["theme"] = theme.value self.meta["theme"] = theme.value
def force_custom_styles(self) -> bool: def set_widget_style(self, style: WidgetStyle) -> None:
return self.meta.get("force_custom_styles", False) self.meta["widget_style"] = style
theme_manager.apply_style()
def set_force_custom_styles(self, enabled: bool) -> None: def get_widget_style(self) -> WidgetStyle:
self.meta["force_custom_styles"] = enabled return self.meta.get(
"widget_style", WidgetStyle.NATIVE if is_mac else WidgetStyle.ANKI
)
def browser_layout(self) -> BrowserLayout: def browser_layout(self) -> BrowserLayout:
from aqt.browser.layout import BrowserLayout from aqt.browser.layout import BrowserLayout

View file

@ -325,7 +325,6 @@ class Reviewer:
self.web.allow_drops = True self.web.allow_drops = True
self.web.eval("_blockDefaultDragDropBehavior();") self.web.eval("_blockDefaultDragDropBehavior();")
# show answer / ease buttons # show answer / ease buttons
self.bottom.web.show()
self.bottom.web.stdHtml( self.bottom.web.stdHtml(
self._bottomHTML(), self._bottomHTML(),
css=["css/toolbar-bottom.css", "css/reviewer-bottom.css"], css=["css/toolbar-bottom.css", "css/reviewer-bottom.css"],
@ -706,7 +705,6 @@ time = %(time)d;
else: else:
maxTime = 0 maxTime = 0
self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime)) self.bottom.web.eval("showQuestion(%s,%d);" % (json.dumps(middle), maxTime))
self.bottom.web.adjustHeightToFit()
def _showEaseButtons(self) -> None: def _showEaseButtons(self) -> None:
middle = self._answerButtons() middle = self._answerButtons()

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,6 @@ from anki.utils import is_lin, is_mac, is_win
from aqt import QApplication, colors, gui_hooks from aqt import QApplication, colors, gui_hooks
from aqt.qt import ( from aqt.qt import (
QColor, QColor,
QGuiApplication,
QIcon, QIcon,
QPainter, QPainter,
QPalette, QPalette,
@ -44,6 +43,11 @@ class ColoredIcon:
return ColoredIcon(path=self.path, color=color) return ColoredIcon(path=self.path, color=color)
class WidgetStyle(enum.IntEnum):
ANKI = 0
NATIVE = 1
class Theme(enum.IntEnum): class Theme(enum.IntEnum):
FOLLOW_SYSTEM = 0 FOLLOW_SYSTEM = 0
LIGHT = 1 LIGHT = 1
@ -56,8 +60,8 @@ class ThemeManager:
_icon_cache_dark: dict[str, QIcon] = {} _icon_cache_dark: dict[str, QIcon] = {}
_icon_size = 128 _icon_size = 128
_dark_mode_available: bool | None = None _dark_mode_available: bool | None = None
default_palette: QPalette | None = None
_default_style: str | None = None _default_style: str | None = None
_current_widget_style: WidgetStyle | None = None
def rtl(self) -> bool: def rtl(self) -> bool:
return is_rtl(anki.lang.current_lang) return is_rtl(anki.lang.current_lang)
@ -168,8 +172,10 @@ class ThemeManager:
classes.extend(["nightMode", "night_mode"]) classes.extend(["nightMode", "night_mode"])
if self.macos_dark_mode(): if self.macos_dark_mode():
classes.append("macos-dark-mode") classes.append("macos-dark-mode")
if aqt.mw.pm.reduced_motion(): if aqt.mw.pm.reduce_motion():
classes.append("reduced-motion") classes.append("reduce-motion")
if not aqt.mw.pm.minimalist_mode():
classes.append("fancy")
if qtmajor == 5 and qtminor < 15: if qtmajor == 5 and qtminor < 15:
classes.append("no-blur") classes.append("no-blur")
return " ".join(classes) return " ".join(classes)
@ -212,56 +218,50 @@ class ThemeManager:
else: else:
return get_linux_dark_mode() return get_linux_dark_mode()
def apply_style_if_system_style_changed(self) -> None:
theme = aqt.mw.pm.theme()
if theme != Theme.FOLLOW_SYSTEM:
return
if self._determine_night_mode() != self.night_mode:
self.apply_style()
def apply_style(self) -> None: def apply_style(self) -> None:
"Apply currently configured style." "Apply currently configured style."
new_theme = self._determine_night_mode()
theme_changed = self.night_mode != new_theme
new_widget_style = aqt.mw.pm.get_widget_style()
style_changed = self._current_widget_style != new_widget_style
if not theme_changed and not style_changed:
return
self.night_mode = new_theme
self._current_widget_style = new_widget_style
app = aqt.mw.app app = aqt.mw.app
self.night_mode = self._determine_night_mode() if not self._default_style:
if not self.default_palette:
self.default_palette = QGuiApplication.palette()
self._default_style = app.style().objectName() self._default_style = app.style().objectName()
self._apply_palette(app) self._apply_palette(app)
self._apply_style(app) self._apply_style(app)
gui_hooks.theme_did_change() gui_hooks.theme_did_change()
def _apply_style(self, app: QApplication) -> None: def _apply_style(self, app: QApplication) -> None:
from aqt.stylesheets import splitter_styles buf = ""
buf = splitter_styles(self) if aqt.mw.pm.get_widget_style() == WidgetStyle.ANKI:
from aqt.stylesheets import custom_styles
if not is_mac or aqt.mw.pm.force_custom_styles(): app.setStyle(QStyleFactory.create("fusion")) # type: ignore
from aqt.stylesheets import (
button_styles,
checkbox_styles,
combobox_styles,
general_styles,
menu_styles,
scrollbar_styles,
spinbox_styles,
table_styles,
tabwidget_styles,
)
buf += "".join( buf += "".join(
[ [
general_styles(self), custom_styles.general(self),
button_styles(self), custom_styles.button(self),
checkbox_styles(self), custom_styles.checkbox(self),
menu_styles(self), custom_styles.menu(self),
combobox_styles(self), custom_styles.combobox(self),
tabwidget_styles(self), custom_styles.tabwidget(self),
table_styles(self), custom_styles.table(self),
spinbox_styles(self), custom_styles.spinbox(self),
scrollbar_styles(self), custom_styles.scrollbar(self),
custom_styles.slider(self),
custom_styles.splitter(self),
] ]
) )
else:
app.setStyle(QStyleFactory.create(self._default_style)) # type: ignore
# allow addons to modify the styling # allow addons to modify the styling
buf = gui_hooks.style_did_init(buf) buf = gui_hooks.style_did_init(buf)
@ -270,19 +270,6 @@ class ThemeManager:
def _apply_palette(self, app: QApplication) -> None: def _apply_palette(self, app: QApplication) -> None:
set_macos_dark_mode(self.night_mode) set_macos_dark_mode(self.night_mode)
if is_mac and not (qtmajor == 5 or aqt.mw.pm.force_custom_styles()):
app.setStyle(QStyleFactory.create(self._default_style)) # type: ignore
self.default_palette.setColor(
QPalette.ColorRole.Window, self.qcolor(colors.CANVAS)
)
self.default_palette.setColor(
QPalette.ColorRole.AlternateBase, self.qcolor(colors.CANVAS)
)
app.setPalette(self.default_palette)
return
app.setStyle(QStyleFactory.create("fusion")) # type: ignore
palette = QPalette() palette = QPalette()
text = self.qcolor(colors.FG) text = self.qcolor(colors.FG)
palette.setColor(QPalette.ColorRole.WindowText, text) palette.setColor(QPalette.ColorRole.WindowText, text)
@ -300,7 +287,7 @@ class ThemeManager:
palette.setColor(QPalette.ColorRole.Window, canvas) palette.setColor(QPalette.ColorRole.Window, canvas)
palette.setColor(QPalette.ColorRole.AlternateBase, canvas) palette.setColor(QPalette.ColorRole.AlternateBase, canvas)
palette.setColor(QPalette.ColorRole.Button, self.qcolor(colors.BUTTON_BG)) palette.setColor(QPalette.ColorRole.Button, canvas)
input_base = self.qcolor(colors.CANVAS_CODE) input_base = self.qcolor(colors.CANVAS_CODE)
palette.setColor(QPalette.ColorRole.Base, input_base) palette.setColor(QPalette.ColorRole.Base, input_base)

View file

@ -2,18 +2,25 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
import enum
import re import re
from typing import Any, Optional from typing import Any, Callable, Optional, cast
import aqt import aqt
from anki.sync import SyncStatus from anki.sync import SyncStatus
from aqt import gui_hooks from aqt import gui_hooks, props
from aqt.qt import * from aqt.qt import *
from aqt.sync import get_sync_status from aqt.sync import get_sync_status
from aqt.theme import theme_manager
from aqt.utils import tr from aqt.utils import tr
from aqt.webview import AnkiWebView from aqt.webview import AnkiWebView
class HideMode(enum.IntEnum):
FULLSCREEN = 0
ALWAYS = 1
# wrapper class for set_bridge_command() # wrapper class for set_bridge_command()
class TopToolbar: class TopToolbar:
def __init__(self, toolbar: Toolbar) -> None: def __init__(self, toolbar: Toolbar) -> None:
@ -27,45 +34,94 @@ class BottomToolbar:
class ToolbarWebView(AnkiWebView): class ToolbarWebView(AnkiWebView):
hide_condition: Callable[..., bool]
def __init__(self, mw: aqt.AnkiQt, title: str) -> None: def __init__(self, mw: aqt.AnkiQt, title: str) -> None:
AnkiWebView.__init__(self, mw, title=title) AnkiWebView.__init__(self, mw, title=title)
self.mw = mw self.mw = mw
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus) self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
self.disable_zoom() self.disable_zoom()
self.collapsed = False self.hidden = False
self.web_height = 0
# collapse timer
self.hide_timer = QTimer() self.hide_timer = QTimer()
self.hide_timer.setSingleShot(True) self.hide_timer.setSingleShot(True)
self.hide_timer.setInterval(1000) self.reset_timer()
qconnect(self.hide_timer.timeout, self.mw.collapse_toolbar_if_allowed)
def reset_timer(self) -> None:
self.hide_timer.stop()
self.hide_timer.setInterval(2000)
def hide(self) -> None:
self.hidden = True
def show(self) -> None:
self.hidden = False
class TopWebView(ToolbarWebView):
def __init__(self, mw: aqt.AnkiQt, title: str) -> None:
super().__init__(mw, title=title)
self.web_height = 0
qconnect(self.hide_timer.timeout, self.hide_if_allowed)
def eventFilter(self, obj, evt): def eventFilter(self, obj, evt):
if handled := super().eventFilter(obj, evt): if handled := super().eventFilter(obj, evt):
return handled return handled
# prevent collapse if pointer inside # prevent collapse of both toolbars if pointer is inside one of them
if evt.type() == QEvent.Type.Enter: if evt.type() == QEvent.Type.Enter:
self.hide_timer.stop() self.reset_timer()
self.hide_timer.setInterval(1000) self.mw.bottomWeb.reset_timer()
return True return True
return False return False
def on_body_classes_need_update(self) -> None:
super().on_body_classes_need_update()
if self.mw.state == "review":
if self.mw.pm.hide_top_bar():
self.eval("""document.body.classList.remove("flat"); """)
else:
self.flatten()
self.show()
def _onHeight(self, qvar: Optional[int]) -> None: def _onHeight(self, qvar: Optional[int]) -> None:
super()._onHeight(qvar) super()._onHeight(qvar)
self.web_height = int(qvar) self.web_height = int(qvar)
def collapse(self) -> None: def hide_if_allowed(self) -> None:
self.collapsed = True if self.mw.state != "review":
self.eval("""document.body.classList.add("collapsed"); """) return
def expand(self) -> None: if self.mw.pm.hide_top_bar():
self.collapsed = False if (
self.eval("""document.body.classList.remove("collapsed"); """) self.mw.pm.top_bar_hide_mode() == HideMode.FULLSCREEN
and not self.mw.windowState() & Qt.WindowState.WindowFullScreen
):
self.show()
return
self.hide()
def hide(self) -> None:
super().hide()
self.hidden = True
self.eval(
"""document.body.classList.add("hidden"); """,
)
if self.mw.fullscreen:
self.mw.hide_menubar()
def show(self) -> None:
super().show()
self.eval("""document.body.classList.remove("hidden"); """)
self.mw.show_menubar()
def flatten(self) -> None: def flatten(self) -> None:
self.eval("document.body.classList.add('flat'); ") self.eval("""document.body.classList.add("flat"); """)
def elevate(self) -> None: def elevate(self) -> None:
self.eval( self.eval(
@ -76,15 +132,24 @@ class ToolbarWebView(AnkiWebView):
) )
def update_background_image(self) -> None: def update_background_image(self) -> None:
def set_background(val: str) -> None: if self.mw.pm.minimalist_mode():
return
def set_background(computed: str) -> None:
# remove offset from copy # remove offset from copy
background = re.sub(r"-\d+px ", "0%", val) background = re.sub(r"-\d+px ", "0%", computed)
# ensure alignment with main webview
background = re.sub(r"\sfixed", "", background)
# change computedStyle px value back to 100vw # change computedStyle px value back to 100vw
background = re.sub(r"\d+px", "100vw", background) background = re.sub(r"\d+px", "100vw", background)
self.eval( self.eval(
f"""document.body.style.setProperty("background", '{background}'); """ f"""
document.body.style.setProperty("background", '{background}');
"""
) )
self.set_body_height(self.mw.web.height())
# offset reviewer background by toolbar height # offset reviewer background by toolbar height
self.mw.web.eval( self.mw.web.eval(
f"""document.body.style.setProperty("background-position-y", "-{self.web_height}px"); """ f"""document.body.style.setProperty("background-position-y", "-{self.web_height}px"); """
@ -95,6 +160,93 @@ class ToolbarWebView(AnkiWebView):
set_background, set_background,
) )
def set_body_height(self, height: int) -> None:
self.eval(
f"""document.body.style.setProperty("min-height", "{self.mw.web.height()}px"); """
)
def adjustHeightToFit(self) -> None:
self.eval("""document.body.style.setProperty("min-height", "0px"); """)
self.evalWithCallback("document.documentElement.offsetHeight", self._onHeight)
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
self.mw.web.evalWithCallback(
"""window.innerHeight; """,
self.set_body_height,
)
class BottomWebView(ToolbarWebView):
def __init__(self, mw: aqt.AnkiQt, title: str) -> None:
super().__init__(mw, title=title)
qconnect(self.hide_timer.timeout, self.hide_if_allowed)
def eventFilter(self, obj, evt):
if handled := super().eventFilter(obj, evt):
return handled
if evt.type() == QEvent.Type.Enter:
self.reset_timer()
self.mw.toolbarWeb.reset_timer()
return True
return False
def on_body_classes_need_update(self) -> None:
super().on_body_classes_need_update()
if self.mw.state == "review":
self.show()
def animate_height(self, height: int) -> None:
self.web_height = height
if self.mw.pm.reduce_motion():
self.setFixedHeight(height)
else:
# Collapse/Expand animation
self.setMinimumHeight(0)
self.animation = QPropertyAnimation(
self, cast(QByteArray, b"maximumHeight")
)
self.animation.setDuration(int(theme_manager.var(props.TRANSITION)))
self.animation.setStartValue(self.height())
self.animation.setEndValue(height)
qconnect(self.animation.finished, lambda: self.setFixedHeight(height))
self.animation.start()
def hide_if_allowed(self) -> None:
if self.mw.state != "review":
return
if self.mw.pm.hide_bottom_bar():
if (
self.mw.pm.bottom_bar_hide_mode() == HideMode.FULLSCREEN
and not self.mw.windowState() & Qt.WindowState.WindowFullScreen
):
self.show()
return
self.hide()
def hide(self) -> None:
super().hide()
self.hidden = True
self.animate_height(1)
def show(self) -> None:
super().show()
self.hidden = False
if self.mw.state == "review":
self.evalWithCallback(
"document.documentElement.offsetHeight", self.animate_height
)
else:
self.adjustHeightToFit()
class Toolbar: class Toolbar:
def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None: def __init__(self, mw: aqt.AnkiQt, web: AnkiWebView) -> None:

View file

@ -248,6 +248,7 @@ class AnkiWebView(QWebEngineView):
self.resetHandlers() self.resetHandlers()
self._filterSet = False self._filterSet = False
gui_hooks.theme_did_change.append(self.on_theme_did_change) gui_hooks.theme_did_change.append(self.on_theme_did_change)
gui_hooks.body_classes_need_update.append(self.on_body_classes_need_update)
qconnect(self.loadFinished, self._on_load_finished) qconnect(self.loadFinished, self._on_load_finished)
@ -410,8 +411,7 @@ class AnkiWebView(QWebEngineView):
return 3 return 3
def standard_css(self) -> str: def standard_css(self) -> str:
palette = theme_manager.default_palette color_hl = theme_manager.var(colors.BORDER_FOCUS)
color_hl = palette.color(QPalette.ColorRole.Highlight).name()
if is_win: if is_win:
# T: include a font for your language on Windows, eg: "Segoe UI", "MS Mincho" # T: include a font for your language on Windows, eg: "Segoe UI", "MS Mincho"
@ -706,6 +706,7 @@ html {{ {font} }}
return return
gui_hooks.theme_did_change.remove(self.on_theme_did_change) gui_hooks.theme_did_change.remove(self.on_theme_did_change)
gui_hooks.body_classes_need_update.remove(self.on_body_classes_need_update)
mw.mediaServer.clear_page_html(id(self)) mw.mediaServer.clear_page_html(id(self))
self._page.deleteLater() self._page.deleteLater()
@ -733,6 +734,16 @@ html {{ {font} }}
""" """
) )
def on_body_classes_need_update(self) -> None:
from aqt import mw
self.eval(
f"""document.body.classList.toggle("fancy", {json.dumps(not mw.pm.minimalist_mode())}); """
)
self.eval(
f"""document.body.classList.toggle("reduce-motion", {json.dumps(mw.pm.minimalist_mode())}); """
)
@deprecated(info="use theme_manager.qcolor() instead") @deprecated(info="use theme_manager.qcolor() instead")
def get_window_bg_color(self, night_mode: Optional[bool] = None) -> QColor: def get_window_bg_color(self, night_mode: Optional[bool] = None) -> QColor:
return theme_manager.qcolor(colors.CANVAS) return theme_manager.qcolor(colors.CANVAS)

40
qt/aqt/widgetgallery.py Normal file
View file

@ -0,0 +1,40 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt
import aqt.main
from aqt.qt import QDialog, qconnect
from aqt.theme import WidgetStyle
from aqt.utils import restoreGeom, saveGeom
class WidgetGallery(QDialog):
silentlyClose = True
def __init__(self, mw: aqt.main.AnkiQt) -> None:
super().__init__(mw)
self.mw = mw.weakref()
self.form = aqt.forms.widgets.Ui_Dialog()
self.form.setupUi(self)
restoreGeom(self, "WidgetGallery")
qconnect(
self.form.disableCheckBox.stateChanged,
lambda: self.form.testGrid.setEnabled(
not self.form.disableCheckBox.isChecked()
),
)
self.form.styleComboBox.addItems(
[member.name.lower().capitalize() for member in WidgetStyle]
)
self.form.styleComboBox.setCurrentIndex(self.mw.pm.get_widget_style())
qconnect(
self.form.styleComboBox.currentIndexChanged,
self.mw.pm.set_widget_style,
)
def reject(self) -> None:
super().reject()
saveGeom(self, "WidgetGallery")

View file

@ -613,6 +613,10 @@ hooks = [
name="theme_did_change", name="theme_did_change",
doc="Called after night mode is toggled.", doc="Called after night mode is toggled.",
), ),
Hook(
name="body_classes_need_update",
doc="Called when a setting involving a webview body class is toggled.",
),
# Webview # Webview
################### ###################
Hook( Hook(

View file

@ -25,8 +25,14 @@ $vars: (
default: 5px, default: 5px,
), ),
), ),
medium: (
"Used for container corners",
(
default: 12px,
),
),
large: ( large: (
"Used for big centered buttons", "Used for pill-shaped buttons",
( (
default: 15px, default: 15px,
), ),

View file

@ -1,6 +1,7 @@
@use "vars" as *; @use "vars" as *;
@use "root-vars"; @use "root-vars";
@use "button-mixins" as button; @use "button-mixins" as button;
@use "sass/scrollbar";
$body-color: color(fg); $body-color: color(fg);
$body-bg: color(canvas); $body-bg: color(canvas);
@ -43,6 +44,26 @@ html {
overscroll-behavior: none; overscroll-behavior: none;
} }
body {
&:not(.isMac),
&:not(.isMac) * {
@include scrollbar.custom;
}
&.reduce-motion,
&.reduce-motion * {
transition: none !important;
animation: none !important;
}
&.no-blur * {
backdrop-filter: none !important;
}
&:not(.fancy),
&:not(.fancy) * {
box-shadow: none !important;
backdrop-filter: none !important;
}
}
button { button {
/* override transition for instant hover response */ /* override transition for instant hover response */
transition: color var(--transition) ease-in-out, box-shadow var(--transition) ease-in-out !important; transition: color var(--transition) ease-in-out, box-shadow var(--transition) ease-in-out !important;
@ -57,7 +78,7 @@ samp {
unicode-bidi: normal !important; unicode-bidi: normal !important;
} }
.reduced-motion * { .reduce-motion * {
transition: none !important; transition: none !important;
animation: none !important; animation: none !important;
} }

View file

@ -24,15 +24,24 @@
button { button {
outline: none !important; outline: none !important;
@include button.base; background: var(--button-bg);
border-radius: var(--border-radius-large); border-radius: var(--border-radius);
padding: 8px 10px; border: 1px solid var(--border-subtle);
&:hover {
background: var(--button-gradient-start);
border: 1px solid var(--border);
}
font-weight: 500; font-weight: 500;
padding: 8px 10px;
margin: 0 4px; margin: 0 4px;
@include elevation(1, $opacity-boost: -0.08); .fancy & {
&:hover { @include button.base;
@include elevation(2); border-radius: var(--border-radius-large);
transition: box-shadow var(--transition) linear; @include elevation(1, $opacity-boost: -0.08);
&:hover {
@include elevation(2);
transition: box-shadow var(--transition) linear;
}
} }
} }

View file

@ -9,7 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let collapse = false; export let collapse = false;
export let toggleDisplay = false; export let toggleDisplay = false;
export let animated = !document.body.classList.contains("reduced-motion"); export let animated = !document.body.classList.contains("reduce-motion");
let contentHeight = 0; let contentHeight = 0;

View file

@ -38,17 +38,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
width: 100%; width: 100%;
background: var(--canvas-elevated); background: var(--canvas-elevated);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--border-radius-large, 10px); border-radius: var(--border-radius-medium, 10px);
padding: 1rem 1.75rem 0.75rem 1.25rem;
&.rtl {
padding: 1rem 1.25rem 0.75rem 1.75rem;
}
&:hover,
&:focus-within {
.help-badge {
color: var(--fg-subtle);
}
}
&.light { &.light {
@include elevation(2, $opacity-boost: -0.08); @include elevation(2, $opacity-boost: -0.08);
&:hover, &:hover,
@ -63,6 +54,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
@include elevation(4); @include elevation(4);
} }
} }
padding: 1rem 1.75rem 0.75rem 1.25rem;
&.rtl {
padding: 1rem 1.25rem 0.75rem 1.75rem;
}
&:hover,
&:focus-within {
.help-badge {
color: var(--fg-subtle);
}
}
transition: box-shadow var(--transition) ease-in-out; transition: box-shadow var(--transition) ease-in-out;
page-break-inside: avoid; page-break-inside: avoid;
} }

View file

@ -158,7 +158,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.modal-content { .modal-content {
background-color: var(--canvas); background-color: var(--canvas);
color: var(--fg); color: var(--fg);
border-radius: var(--border-radius-large, 10px); border-radius: var(--border-radius-medium, 10px);
} }
.invert { .invert {

View file

@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context as editorFieldContext } from "./EditorField.svelte"; import { context as editorFieldContext } from "./EditorField.svelte";
import { plainTextIcon } from "./icons"; import { plainTextIcon } from "./icons";
const animated = !document.body.classList.contains("reduced-motion"); const animated = !document.body.classList.contains("reduce-motion");
const editorField = editorFieldContext.get(); const editorField = editorFieldContext.get();
const keyCombination = "Control+Shift+X"; const keyCombination = "Control+Shift+X";

View file

@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context as editorFieldContext } from "./EditorField.svelte"; import { context as editorFieldContext } from "./EditorField.svelte";
import { richTextIcon } from "./icons"; import { richTextIcon } from "./icons";
const animated = !document.body.classList.contains("reduced-motion"); const animated = !document.body.classList.contains("reduce-motion");
const editorField = editorFieldContext.get(); const editorField = editorFieldContext.get();
const keyCombination = "Control+Shift+X"; const keyCombination = "Control+Shift+X";

View file

@ -12,7 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context as editorFieldContext } from "./EditorField.svelte"; import { context as editorFieldContext } from "./EditorField.svelte";
import { stickyIcon } from "./icons"; import { stickyIcon } from "./icons";
const animated = !document.body.classList.contains("reduced-motion"); const animated = !document.body.classList.contains("reduce-motion");
export let active: boolean; export let active: boolean;
export let show: boolean; export let show: boolean;

View file

@ -11,8 +11,9 @@ body {
margin: 20px; margin: 20px;
overflow-wrap: break-word; overflow-wrap: break-word;
// default background setting to fit with toolbar // default background setting to fit with toolbar
background-size: 100vw; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: top;
background-attachment: fixed; background-attachment: fixed;
} }