With no art, the sidebar's art widget moves out of the playlist list's frame onto the window below it, so the panel is set into the gray like the visualizer rather than into a white box. A tall debossed panel is now shaded along its top and bottom exactly as the 62 px visualizer is, flat in the visualizer's color between, instead of one gradient stretched over its whole height, which read as gray. The scrollbar thumb is shaded the same way, like a button.
177 lines
6.5 KiB
Python
177 lines
6.5 KiB
Python
"""Round 80: scrollbars lose their arrow buttons, and the thumb is a button.
|
|
|
|
- No arrows: the trough runs the bar's full length and the arrow rects are
|
|
empty, so nothing paints or hit-tests there.
|
|
- The thumb gets the buttons' corner radius and emboss, with rims as tall in
|
|
pixels as a button's, however long the thumb.
|
|
- The bar's thickness comes from the UI scale through the style, not QSS
|
|
(any QSS rule on a scrollbar part makes Qt draw the arrows itself).
|
|
"""
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtGui import QColor
|
|
from PyQt6.QtWidgets import QPushButton, QScrollBar, QStyle, QStyleOptionSlider
|
|
|
|
from lintunes import theme
|
|
from lintunes.gui.track_table import ClickToJumpScrollStyle
|
|
|
|
SC = QStyle.SubControl
|
|
|
|
|
|
def _option(bar):
|
|
opt = QStyleOptionSlider()
|
|
bar.initStyleOption(opt)
|
|
return opt
|
|
|
|
|
|
def _bar(qapp, orientation=Qt.Orientation.Vertical):
|
|
bar = QScrollBar(orientation)
|
|
bar.setStyle(ClickToJumpScrollStyle())
|
|
bar.setRange(0, 900)
|
|
bar.setPageStep(100)
|
|
bar.resize(14, 400) if orientation == Qt.Orientation.Vertical \
|
|
else bar.resize(400, 14)
|
|
return bar
|
|
|
|
|
|
def test_no_arrow_buttons(qapp):
|
|
bar = _bar(qapp)
|
|
style, opt = bar.style(), _option(bar)
|
|
cc = QStyle.ComplexControl.CC_ScrollBar
|
|
assert style.subControlRect(cc, opt, SC.SC_ScrollBarAddLine, bar).isEmpty()
|
|
assert style.subControlRect(cc, opt, SC.SC_ScrollBarSubLine, bar).isEmpty()
|
|
assert style.subControlRect(cc, opt, SC.SC_ScrollBarGroove, bar) == opt.rect
|
|
|
|
|
|
def test_thumb_spans_the_full_trough(qapp):
|
|
for orientation in (Qt.Orientation.Vertical, Qt.Orientation.Horizontal):
|
|
bar = _bar(qapp, orientation)
|
|
style, cc = bar.style(), QStyle.ComplexControl.CC_ScrollBar
|
|
bar.setValue(0)
|
|
top = style.subControlRect(cc, _option(bar), SC.SC_ScrollBarSlider, bar)
|
|
bar.setValue(900)
|
|
end = style.subControlRect(cc, _option(bar), SC.SC_ScrollBarSlider, bar)
|
|
if orientation == Qt.Orientation.Vertical:
|
|
assert top.top() == 0 and end.bottom() == 399
|
|
assert top.height() == 40 # 100 / (900 + 100) of 400
|
|
else:
|
|
assert top.left() == 0 and end.right() == 399
|
|
|
|
|
|
def test_tiny_page_keeps_a_grabbable_thumb(qapp):
|
|
bar = _bar(qapp)
|
|
bar.setRange(0, 100000)
|
|
rect = bar.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar,
|
|
_option(bar), SC.SC_ScrollBarSlider, bar)
|
|
assert rect.height() == ClickToJumpScrollStyle.extent * 2
|
|
|
|
|
|
def test_thickness_follows_ui_scale(qapp):
|
|
class Prefs(dict):
|
|
def get(self, key, default=None):
|
|
return "large" if key == "ui_scale" else default
|
|
before = ClickToJumpScrollStyle.extent
|
|
try:
|
|
theme.apply_theme(qapp, Prefs())
|
|
assert ClickToJumpScrollStyle.extent == theme.UI_SCALES["large"]["scrollbar"]
|
|
assert "QScrollBar" not in qapp.styleSheet()
|
|
finally:
|
|
ClickToJumpScrollStyle.extent = before
|
|
|
|
|
|
def test_thumb_is_shaded_like_a_real_button(qapp):
|
|
qapp.setStyleSheet(theme.button_emboss_qss(QColor("#d0d0d0")))
|
|
try:
|
|
button = QPushButton("OK")
|
|
assert abs(theme.button_height_px(button.fontMetrics())
|
|
- button.sizeHint().height()) <= 2
|
|
finally:
|
|
qapp.setStyleSheet("")
|
|
|
|
|
|
def test_thumb_paints(qapp):
|
|
bar = _bar(qapp)
|
|
bar.setValue(450)
|
|
image = bar.grab().toImage()
|
|
thumb = bar.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar,
|
|
_option(bar), SC.SC_ScrollBarSlider, bar)
|
|
trough = image.pixelColor(7, 5)
|
|
middle = image.pixelColor(7, thumb.center().y())
|
|
assert middle != trough
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# no album art: a debossed panel on the window, not a box in the list
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _sidebar(qapp, tmp_path):
|
|
from lintunes.gui.main_window import MainWindow
|
|
from lintunes.library_manager import LibraryManager
|
|
from lintunes.models.library import Library
|
|
from lintunes.models.track import Track
|
|
from lintunes.preferences import Preferences
|
|
library = Library(tracks={1: Track(track_id=1, name="One")})
|
|
window = MainWindow(LibraryManager(library, tmp_path), Preferences(tmp_path))
|
|
window.resize(1000, 700)
|
|
window.show()
|
|
qapp.processEvents()
|
|
return window, window.sidebar
|
|
|
|
|
|
def test_no_art_panel_sits_below_the_frame(qapp, tmp_path):
|
|
from lintunes.gui.sidebar import NO_ART_GAP
|
|
window, sidebar = _sidebar(qapp, tmp_path)
|
|
art = sidebar.art
|
|
assert not art.has_art()
|
|
assert art.parentWidget() is sidebar
|
|
assert art.y() >= sidebar.list_area.geometry().bottom()
|
|
assert art.height() == art.width() + NO_ART_GAP
|
|
image = art.grab().toImage()
|
|
window_bg = art.palette().window().color()
|
|
# the gap above is bare window; the panel's middle is the visualizer's
|
|
# color, flat, not shaded gray over its whole height
|
|
assert image.pixelColor(art.width() // 2, 1) == window_bg
|
|
panel = art.palette().alternateBase().color()
|
|
for y in (art.height() // 3, art.height() // 2, 2 * art.height() // 3):
|
|
assert image.pixelColor(art.width() // 2, y) == panel
|
|
window.close()
|
|
|
|
|
|
def test_real_art_goes_inside_the_frame(qapp, tmp_path):
|
|
from PyQt6.QtGui import QPixmap
|
|
window, sidebar = _sidebar(qapp, tmp_path)
|
|
art = sidebar.art
|
|
pixmap = QPixmap(10, 10)
|
|
pixmap.fill(QColor("red"))
|
|
art._pixmap = pixmap
|
|
art.has_art_changed.emit(True)
|
|
art._fit_height()
|
|
assert art.parentWidget() is sidebar.list_area
|
|
assert art.height() == art.width()
|
|
art._pixmap = None
|
|
art.set_track(None)
|
|
assert art.parentWidget() is sidebar
|
|
window.close()
|
|
|
|
|
|
def test_tall_deboss_copies_a_short_panels_edges(qapp):
|
|
"""Shaded ``like`` a 62 px panel, a 400 px one matches it row for row
|
|
along the top, and is flat in the middle."""
|
|
from PyQt6.QtCore import QRectF
|
|
from PyQt6.QtGui import QImage, QPainter
|
|
color = QColor("#f2f2f2")
|
|
|
|
def paint(height, **kw):
|
|
image = QImage(100, height, QImage.Format.Format_ARGB32)
|
|
image.fill(0)
|
|
painter = QPainter(image)
|
|
theme.paint_deboss(painter, QRectF(0.5, 0.5, 99, height - 1),
|
|
color, 8, **kw)
|
|
painter.end()
|
|
return image
|
|
|
|
short, tall = paint(62), paint(400, like=62)
|
|
for y in (3, 10, 20):
|
|
assert abs(short.pixelColor(50, y).lightness()
|
|
- tall.pixelColor(50, y).lightness()) <= 1
|
|
assert tall.pixelColor(50, 200) == color
|