anki/qt/aqt/stylesheets.py
Matthias Metelka f169ee0933
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
2023-01-18 21:24:16 +10:00

611 lines
17 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.utils import is_win
from aqt import colors, props
from aqt.theme import ThemeManager
def button_gradient(start: str, end: str) -> str:
return f"""
qlineargradient(
spread:pad, x1:0.5, y1:0, x2:0.5, y2:1,
stop:0 {start},
stop:1 {end}
);
"""
def button_pressed_gradient(start: str, end: str, shadow: str) -> str:
return f"""
qlineargradient(
spread:pad, x1:0.5, y1:0, x2:0.5, y2:1,
stop:0 {shadow},
stop:0.1 {start},
stop:0.9 {end},
stop:1 {shadow}
);
"""
class CustomStyles:
def general(self, tm: ThemeManager) -> str:
return f"""
QFrame,
QWidget {{
background: none;
}}
QPushButton,
QComboBox,
QSpinBox,
QDateTimeEdit,
QLineEdit,
QListWidget,
QTreeWidget,
QListView,
QTextEdit,
QPlainTextEdit {{
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-radius: {tm.var(props.BORDER_RADIUS)};
}}
QLineEdit,
QTextEdit,
QPlainTextEdit,
QDateTimeEdit,
QListWidget,
QTreeWidget,
QListView {{
background: {tm.var(colors.CANVAS_CODE)};
}}
QLineEdit,
QTextEdit,
QPlainTextEdit,
QDateTimeEdit {{
padding: 2px;
}}
QSpinBox:focus,
QDateTimeEdit:focus,
QLineEdit:focus,
QTextEdit:editable:focus,
QPlainTextEdit:editable:focus,
QWidget:editable:focus {{
border-color: {tm.var(colors.BORDER_FOCUS)};
}}
QPushButton {{
margin-top: 1px;
}}
QPushButton,
QComboBox,
QSpinBox {{
padding: 2px 6px;
}}
QGroupBox {{
text-align: center;
font-weight: bold;
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
padding: 0.75em 0 0.75em 0;
background: {tm.var(colors.CANVAS_ELEVATED)};
border-radius: {tm.var(props.BORDER_RADIUS)};
margin-top: 10px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
margin: 0 2px;
left: 15px;
}}
"""
def menu(self, tm: ThemeManager) -> str:
return f"""
QMenuBar {{
border-bottom: 1px solid {tm.var(colors.BORDER_SUBTLE)};
}}
QMenuBar::item {{
background-color: transparent;
padding: 2px 4px;
border-radius: {tm.var(props.BORDER_RADIUS)};
}}
QMenuBar::item:selected {{
background-color: {tm.var(colors.CANVAS_ELEVATED)};
}}
QMenu {{
background-color: {tm.var(colors.CANVAS_OVERLAY)};
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
padding: 4px;
}}
QMenu::item {{
background-color: transparent;
padding: 3px 14px;
margin-bottom: 4px;
}}
QMenu::item:selected {{
background-color: {tm.var(colors.HIGHLIGHT_BG)};
border-radius: {tm.var(props.BORDER_RADIUS)};
}}
QMenu::separator {{
height: 1px;
background: {tm.var(colors.BORDER_SUBTLE)};
margin: 0 8px 4px 8px;
}}
QMenu::indicator {{
border: 1px solid {tm.var(colors.BORDER)};
margin-{tm.left()}: 6px;
margin-{tm.right()}: -6px;
}}
"""
def button(self, tm: ThemeManager) -> str:
# For some reason, Windows needs a larger padding to look the same
button_pad = 25 if is_win else 15
return f"""
QPushButton {{ padding-left: {button_pad}px; padding-right: {button_pad}px; }}
QPushButton,
QTabBar::tab:!selected,
QComboBox:!editable,
QComboBox::drop-down:editable {{
background: {tm.var(colors.BUTTON_BG)};
border-bottom: 1px solid {tm.var(colors.SHADOW)};
}}
QPushButton:default {{
border: 1px solid {tm.var(colors.BORDER_FOCUS)};
}}
QPushButton:focus {{
border: 2px solid {tm.var(colors.BORDER_FOCUS)};
outline: none;
}}
QPushButton:hover,
QTabBar::tab:hover,
QComboBox:!editable:hover,
QSpinBox::up-button:hover,
QSpinBox::down-button:hover,
QDateTimeEdit::up-button:hover,
QDateTimeEdit::down-button:hover {{
background: {
button_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
)
};
}}
QPushButton:default:hover {{
border-width: 2px;
}}
QPushButton:pressed,
QPushButton:checked,
QSpinBox::up-button:pressed,
QSpinBox::down-button:pressed,
QDateTimeEdit::up-button:pressed,
QDateTimeEdit::down-button:pressed {{
background: {
button_pressed_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
)
};
}}
QPushButton:flat {{
border: none;
}}
"""
def splitter(self, tm: ThemeManager) -> str:
return f"""
QSplitter::handle,
QMainWindow::separator {{
height: 16px;
}}
QSplitter::handle:vertical,
QMainWindow::separator:horizontal {{
image: url({tm.themed_icon("mdi:drag-horizontal-FG_SUBTLE")});
}}
QSplitter::handle:horizontal,
QMainWindow::separator:vertical {{
image: url({tm.themed_icon("mdi:drag-vertical-FG_SUBTLE")});
}}
"""
def combobox(self, tm: ThemeManager) -> str:
return f"""
QComboBox {{
padding: {"1px 6px 2px 4px" if tm.rtl() else "1px 4px 2px 6px"};
}}
QComboBox:focus {{
border-color: {tm.var(colors.BORDER_FOCUS)};
}}
QComboBox:editable:on,
QComboBox:editable:focus,
QComboBox::drop-down:focus:editable,
QComboBox::drop-down:pressed {{
border-color: {tm.var(colors.BORDER_FOCUS)};
}}
QComboBox:on {{
border-bottom: none;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}}
QComboBox::item {{
color: {tm.var(colors.FG)};
background: {tm.var(colors.CANVAS_ELEVATED)};
}}
QComboBox::item:selected {{
background: {tm.var(colors.HIGHLIGHT_BG)};
color: {tm.var(colors.HIGHLIGHT_FG)};
}}
QComboBox::item::icon:selected {{
position: absolute;
}}
QComboBox::drop-down {{
subcontrol-origin: border;
padding: 2px;
padding-left: 4px;
padding-right: 4px;
width: 16px;
subcontrol-position: top right;
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
border-bottom-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
}}
QComboBox::drop-down:!editable {{
background: none;
border-color: transparent;
}}
QComboBox::down-arrow {{
image: url({tm.themed_icon("mdi:chevron-down")});
}}
QComboBox::drop-down:hover:editable {{
background: {
button_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
)
};
}}
"""
def tabwidget(self, tm: ThemeManager) -> str:
return f"""
QTabWidget {{
border-radius: {tm.var(props.BORDER_RADIUS)};
background: none;
}}
QTabWidget::pane {{
top: -15px;
padding-top: 1em;
background: {tm.var(colors.CANVAS_ELEVATED)};
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-radius: {tm.var(props.BORDER_RADIUS)};
}}
QTabWidget::tab-bar {{
alignment: center;
}}
QTabBar::tab {{
background: none;
padding: 4px 8px;
min-width: 8ex;
}}
QTabBar::tab {{
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-bottom-color: {tm.var(colors.SHADOW)};
}}
QTabBar::tab:first {{
border-top-{tm.left()}-radius: {tm.var(props.BORDER_RADIUS)};
border-bottom-{tm.left()}-radius: {tm.var(props.BORDER_RADIUS)};
}}
QTabBar::tab:!first {{
margin-{tm.left()}: -1px;
}}
QTabBar::tab:last {{
border-top-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
border-bottom-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
}}
QTabBar::tab:selected {{
color: white;
background: {tm.var(colors.BUTTON_PRIMARY_BG)};
}}
QTabBar::tab:selected:hover {{
background: {
button_gradient(
tm.var(colors.BUTTON_PRIMARY_GRADIENT_START),
tm.var(colors.BUTTON_PRIMARY_GRADIENT_END),
)
};
}}
QTabBar::tab:focus {{
outline: none;
}}
QTabBar::tab:disabled,
QTabBar::tab:disabled:hover {{
background: {tm.var(colors.BUTTON_DISABLED)};
color: {tm.var(colors.FG_DISABLED)};
}}
QTabBar::tab:selected:disabled,
QTabBar::tab:selected:hover:disabled {{
background: {tm.var(colors.BUTTON_PRIMARY_DISABLED)};
}}
"""
def table(self, tm: ThemeManager) -> str:
return f"""
QTableView {{
border-radius: {tm.var(props.BORDER_RADIUS)};
border-{tm.left()}: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-bottom: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
gridline-color: {tm.var(colors.BORDER_SUBTLE)};
selection-background-color: {tm.var(colors.SELECTED_BG)};
selection-color: {tm.var(colors.SELECTED_FG)};
background: {tm.var(colors.CANVAS_CODE)};
}}
QHeaderView {{
background: {tm.var(colors.CANVAS)};
}}
QHeaderView::section {{
padding-{tm.left()}: 0px;
padding-{tm.right()}: 15px;
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
background: {tm.var(colors.BUTTON_BG)};
}}
QHeaderView::section:first {{
margin-left: -1px;
}}
QHeaderView::section:pressed,
QHeaderView::section:pressed:!first {{
background: {
button_pressed_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
tm.var(colors.SHADOW)
)
}
}}
QHeaderView::section:hover {{
background: {
button_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
)
};
}}
QHeaderView::section:first {{
border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-left-radius: {tm.var(props.BORDER_RADIUS)};
}}
QHeaderView::section:!first {{
border-left: none;
}}
QHeaderView::section:last {{
border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-right-radius: {tm.var(props.BORDER_RADIUS)};
}}
QHeaderView::section:only-one {{
border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-left-radius: {tm.var(props.BORDER_RADIUS)};
border-top-right-radius: {tm.var(props.BORDER_RADIUS)};
}}
QHeaderView::up-arrow,
QHeaderView::down-arrow {{
width: 20px;
height: 20px;
margin-{tm.left()}: -20px;
}}
QHeaderView::up-arrow {{
image: url({tm.themed_icon("mdi:menu-up")});
}}
QHeaderView::down-arrow {{
image: url({tm.themed_icon("mdi:menu-down")});
}}
"""
def spinbox(self, tm: ThemeManager) -> str:
return f"""
QSpinBox::up-button,
QSpinBox::down-button,
QDateTimeEdit::up-button,
QDateTimeEdit::down-button {{
subcontrol-origin: border;
width: 16px;
margin: 1px;
}}
QSpinBox::up-button,
QDateTimeEdit::up-button {{
margin-bottom: -1px;
subcontrol-position: top right;
border-top-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
}}
QSpinBox::down-button,
QDateTimeEdit::down-button {{
margin-top: -1px;
subcontrol-position: bottom right;
border-bottom-{tm.right()}-radius: {tm.var(props.BORDER_RADIUS)};
}}
QSpinBox::up-arrow,
QDateTimeEdit::up-arrow {{
image: url({tm.themed_icon("mdi:chevron-up")});
}}
QSpinBox::down-arrow,
QDateTimeEdit::down-arrow {{
image: url({tm.themed_icon("mdi:chevron-down")});
}}
QSpinBox::up-arrow,
QSpinBox::down-arrow,
QSpinBox::up-arrow:pressed,
QSpinBox::down-arrow:pressed,
QSpinBox::up-arrow:disabled:hover, QSpinBox::up-arrow:off:hover,
QSpinBox::down-arrow:disabled:hover, QSpinBox::down-arrow:off:hover,
QDateTimeEdit::up-arrow,
QDateTimeEdit::down-arrow,
QDateTimeEdit::up-arrow:pressed,
QDateTimeEdit::down-arrow:pressed,
QDateTimeEdit::up-arrow:disabled:hover, QDateTimeEdit::up-arrow:off:hover,
QDateTimeEdit::down-arrow:disabled:hover, QDateTimeEdit::down-arrow:off:hover {{
width: 16px;
height: 16px;
}}
QSpinBox::up-arrow:hover,
QSpinBox::down-arrow:hover,
QDateTimeEdit::up-arrow:hover,
QDateTimeEdit::down-arrow:hover {{
width: 20px;
height: 20px;
}}
QSpinBox::up-button:disabled, QSpinBox::up-button:off,
QSpinBox::down-button:disabled, QSpinBox::down-button:off,
QDateTimeEdit::up-button:disabled, QDateTimeEdit::up-button:off,
QDateTimeEdit::down-button:disabled, QDateTimeEdit::down-button:off {{
background: {tm.var(colors.BUTTON_DISABLED)};
}}
QSpinBox::up-arrow:off,
QDateTimeEdit::up-arrow:off {{
image: url({tm.themed_icon("mdi:chevron-up-FG_DISABLED")});
}}
QSpinBox::down-arrow:off,
QDateTimeEdit::down-arrow:off {{
image: url({tm.themed_icon("mdi:chevron-down-FG_DISABLED")});
}}
"""
def checkbox(self, tm: ThemeManager) -> str:
return f"""
QCheckBox,
QRadioButton {{
spacing: 8px;
margin: 2px 0;
}}
QCheckBox::indicator,
QRadioButton::indicator,
QMenu::indicator {{
border: 1px solid {tm.var(colors.BORDER)};
border-radius: {tm.var(props.BORDER_RADIUS)};
background: {tm.var(colors.CANVAS_ELEVATED)};
width: 16px;
height: 16px;
}}
QRadioButton::indicator,
QMenu::indicator:exclusive {{
border-radius: 8px;
}}
QCheckBox::indicator:focus,
QCheckBox::indicator:hover,
QCheckBox::indicator:checked:hover,
QRadioButton::indicator:focus,
QRadioButton::indicator:hover,
QRadioButton::indicator:checked:hover {{
border: 2px solid {tm.var(colors.BORDER_STRONG)};
width: 14px;
height: 14px;
}}
QCheckBox::indicator:checked,
QRadioButton::indicator:checked,
QMenu::indicator:checked {{
image: url({tm.themed_icon("mdi:check")});
}}
QRadioButton::indicator:checked {{
image: url({tm.themed_icon("mdi:circle-medium")});
}}
QCheckBox::indicator:indeterminate {{
image: url({tm.themed_icon("mdi:minus-thick")});
}}
"""
def scrollbar(self, tm: ThemeManager) -> str:
return f"""
QAbstractScrollArea::corner {{
background: none;
border: none;
}}
QScrollBar {{
subcontrol-origin: content;
background-color: transparent;
}}
QScrollBar::handle {{
border-radius: {tm.var(props.BORDER_RADIUS)};
background-color: {tm.var(colors.SCROLLBAR_BG)};
}}
QScrollBar::handle:hover {{
background-color: {tm.var(colors.SCROLLBAR_BG_HOVER)};
}}
QScrollBar::handle:pressed {{
background-color: {tm.var(colors.SCROLLBAR_BG_ACTIVE)};
}}
QScrollBar:horizontal {{
height: 12px;
}}
QScrollBar::handle:horizontal {{
min-width: 60px;
}}
QScrollBar:vertical {{
width: 12px;
}}
QScrollBar::handle:vertical {{
min-height: 60px;
}}
QScrollBar::add-line {{
border: none;
background: none;
}}
QScrollBar::sub-line {{
border: none;
background: none;
}}
"""
def slider(self, tm: ThemeManager) -> str:
return f"""
QSlider::horizontal {{
height: 20px;
}}
QSlider::vertical {{
width: 20px;
}}
QSlider::groove {{
border: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-radius: 3px;
background: {tm.var(colors.CANVAS_ELEVATED)};
}}
QSlider::sub-page {{
background: {tm.var(colors.BUTTON_PRIMARY_GRADIENT_START)};
border-radius: 3px;
margin: 1px;
}}
QSlider::sub-page:disabled {{
background: {tm.var(colors.BUTTON_DISABLED)};
}}
QSlider::add-page {{
margin-{tm.right()}: 2px;
}}
QSlider::groove:vertical {{
width: 6px;
}}
QSlider::groove:horizontal {{
height: 6px;
}}
QSlider::handle {{
background: {tm.var(colors.BUTTON_BG)};
border: 1px solid {tm.var(colors.BORDER)};
border-radius: 9px;
width: 18px;
height: 18px;
border-bottom-color: {tm.var(colors.SHADOW)};
}}
QSlider::handle:vertical {{
margin: 0 -7px;
}}
QSlider::handle:horizontal {{
margin: -7px 0;
}}
QSlider::handle:hover {{
background: {button_gradient(
tm.var(colors.BUTTON_GRADIENT_START),
tm.var(colors.BUTTON_GRADIENT_END),
)}
}}
"""
custom_styles = CustomStyles()