Five grayscale (black->white) sliders in Preferences that update live:
outer-chrome background (QPalette.Window), the rounded transport button
boxes, the now-playing panel, the now-playing text, and the row stripes
(QPalette.AlternateBase). Defaults are None ("inherit current") so nothing
changes until the user drags. Decouples the button boxes from the stripe
color (they used to share palette alternate-base). Live preview via
Preferences.set_live (no disk write); persisted ~0.4s after the drag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
6.5 KiB
Python
169 lines
6.5 KiB
Python
"""Round 11: per-area grayscale color preferences.
|
|
|
|
Five sliders (black->white) tune: the outer chrome background, the rounded
|
|
button boxes, the now-playing panel, the now-playing text, and the row stripes.
|
|
Defaults are None ("inherit current") so nothing changes until the user drags.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from PyQt6.QtCore import QObject, pyqtSignal
|
|
from PyQt6.QtGui import QPalette, QColor
|
|
|
|
from lintunes import theme
|
|
from lintunes.models import Library, Track
|
|
from lintunes.library_manager import LibraryManager
|
|
from lintunes.preferences import Preferences
|
|
from lintunes import player as player_module
|
|
from lintunes.player import Player
|
|
from lintunes.gui.transport import TransportBar
|
|
from lintunes.gui.preferences_dialog import PreferencesDialog, _COLOR_ROWS
|
|
|
|
|
|
def _mock_player():
|
|
library = Library(tracks={1: Track(track_id=1, name="One")})
|
|
with patch.multiple(
|
|
player_module,
|
|
QMediaPlayer=MagicMock(), QAudioOutput=MagicMock(),
|
|
QAudioBufferOutput=MagicMock(), QMediaDevices=MagicMock(),
|
|
):
|
|
return Player(MagicMock(library=library))
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# theme grayscale helpers
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestThemeGrays:
|
|
def test_gray_builds_opaque_gray_clamped(self):
|
|
assert theme.gray(128) == QColor(128, 128, 128)
|
|
assert theme.gray(-5) == QColor(0, 0, 0)
|
|
assert theme.gray(999) == QColor(255, 255, 255)
|
|
|
|
def test_fixed_defaults(self):
|
|
assert theme.default_gray("color_now_playing_bg") == 255
|
|
assert theme.default_gray("color_now_playing_text") == 0
|
|
|
|
def test_gray_value_uses_default_when_unset(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
assert prefs.get("color_now_playing_text") is None
|
|
assert theme.gray_value(prefs, "color_now_playing_text") == 0
|
|
prefs.set("color_now_playing_text", 200)
|
|
assert theme.gray_value(prefs, "color_now_playing_text") == 200
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# apply_theme palette overrides (background -> Window, stripe -> AlternateBase)
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestApplyThemeOverrides:
|
|
def test_background_and_stripe_recolor_palette(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("color_background", 100)
|
|
prefs.set("color_stripe", 40)
|
|
saved = QPalette(qapp.palette())
|
|
try:
|
|
theme.apply_theme(qapp, prefs)
|
|
pal = qapp.palette()
|
|
assert pal.color(QPalette.ColorGroup.Active,
|
|
QPalette.ColorRole.Window) == theme.gray(100)
|
|
assert pal.color(QPalette.ColorGroup.Active,
|
|
QPalette.ColorRole.AlternateBase) == theme.gray(40)
|
|
finally:
|
|
qapp.setPalette(saved)
|
|
|
|
def test_unset_leaves_base_window(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
saved = QPalette(qapp.palette())
|
|
try:
|
|
theme.apply_theme(qapp, prefs) # no overrides set
|
|
base_window = theme._base().color(
|
|
QPalette.ColorGroup.Active, QPalette.ColorRole.Window)
|
|
assert qapp.palette().color(QPalette.ColorGroup.Active,
|
|
QPalette.ColorRole.Window) == base_window
|
|
finally:
|
|
qapp.setPalette(saved)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# TransportBar applies the stylesheet-side colors (button boxes, panel, text)
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestTransportColors:
|
|
def _build(self, tmp_path, prefs):
|
|
manager = LibraryManager(
|
|
Library(tracks={1: Track(track_id=1, name="One")}), tmp_path)
|
|
return TransportBar(_mock_player(), manager, prefs)
|
|
|
|
def test_colors_reflected_at_build(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("color_button", 50)
|
|
prefs.set("color_now_playing_bg", 100)
|
|
prefs.set("color_now_playing_text", 200)
|
|
transport = self._build(tmp_path, prefs)
|
|
|
|
assert transport._boxes # transport, shuffle, bpm
|
|
for box in transport._boxes:
|
|
assert theme.gray(50).name() in box.styleSheet()
|
|
assert theme.gray(100).name() in transport._now_playing.styleSheet()
|
|
assert theme.gray(200).name() in transport._title_label.styleSheet()
|
|
|
|
def test_refresh_theme_repaints_after_change(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
transport = self._build(tmp_path, prefs)
|
|
assert "#000000" in transport._title_label.styleSheet() # default black
|
|
|
|
prefs.set_live("color_now_playing_text", 255)
|
|
transport.refresh_theme()
|
|
assert "#ffffff" in transport._title_label.styleSheet()
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Preferences.set_live: notify without persisting
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestSetLive:
|
|
def test_updates_memory_and_emits_without_writing(self, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
seen = []
|
|
prefs.changed.connect(lambda: seen.append(True))
|
|
|
|
prefs.set_live("color_background", 123)
|
|
|
|
assert prefs.get("color_background") == 123
|
|
assert seen == [True]
|
|
assert not (tmp_path / "preferences.json").exists()
|
|
|
|
prefs.save()
|
|
assert Preferences(tmp_path).get("color_background") == 123
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# PreferencesDialog colors group
|
|
# --------------------------------------------------------------------------
|
|
|
|
class _FakeLastFm(QObject):
|
|
login_finished = pyqtSignal(bool, str)
|
|
status_message = pyqtSignal(str)
|
|
|
|
def is_logged_in(self):
|
|
return False
|
|
|
|
|
|
class TestPreferencesDialogColors:
|
|
def test_sliders_built_and_drive_set_live(self, qapp, tmp_path):
|
|
prefs = Preferences(tmp_path)
|
|
dialog = PreferencesDialog(prefs, _FakeLastFm())
|
|
|
|
keys = [k for k, _ in _COLOR_ROWS]
|
|
assert set(dialog._color_sliders) == set(keys)
|
|
for key in keys:
|
|
assert dialog._color_sliders[key].value() == theme.gray_value(prefs, key)
|
|
|
|
slider = dialog._color_sliders["color_stripe"]
|
|
target = 0 if slider.value() != 0 else 255
|
|
slider.setValue(target)
|
|
|
|
assert prefs.get("color_stripe") == target
|
|
assert dialog._color_save_timer.isActive()
|