v0.5.1: one cast control instead of two
The cast glyph showed up both under the volume slider and inside the visualizer, which was redundant. Dropped the volume-slider indicator (gui/cast_indicator.py deleted) and made the visualizer panel the single cast control — the volume slider goes back to sitting on its own, and nothing shifts position when a session starts. The panel's glyph is bigger (20 -> 40px) and always painted in the theme highlight rather than following the brightness mode. Clicking it now stops casting instead of cycling on/dim/off, which meant nothing when there are no bars to dim; that click was the useful behavior the volume-slider icon had, so it moves here rather than being lost. transport_icon() takes a size and scales the painter, so the bigger glyph is repainted crisply rather than being a blown-up 20px pixmap; size joins the cache key. The cast/cast_connected glyph pair collapses into one, since only the connected state was ever drawn. 450 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+60
-34
@@ -29,7 +29,6 @@ from lintunes.cast.support import (
|
||||
content_type_for, image_type_for, local_ip_for, parse_range,
|
||||
uncastable_reason)
|
||||
from lintunes.gui.cast_dialog import ChromecastDialog
|
||||
from lintunes.gui.cast_indicator import CastIndicator
|
||||
from lintunes.gui.icons import transport_icon
|
||||
from lintunes.gui.spinner import Spinner
|
||||
from lintunes.models import Track
|
||||
@@ -866,32 +865,8 @@ def _device(uuid="u1", name="Kitchen", model="Chromecast Audio"):
|
||||
host="10.0.0.5", port=8009)
|
||||
|
||||
|
||||
class TestCastIndicator:
|
||||
def test_holds_its_space_when_idle(self, qapp):
|
||||
indicator = CastIndicator()
|
||||
indicator.show()
|
||||
# Hiding it would collapse the layout and shift the volume slider the
|
||||
# moment you connect — idle is a visible widget with no icon.
|
||||
assert indicator.icon().isNull()
|
||||
assert not indicator.isEnabled()
|
||||
idle_height = indicator.sizeHint().height()
|
||||
|
||||
indicator.set_connected(False)
|
||||
assert not indicator.isHidden() # never hides itself
|
||||
|
||||
indicator.set_connected(True, "Kitchen")
|
||||
assert not indicator.icon().isNull()
|
||||
assert indicator.isEnabled()
|
||||
assert "Kitchen" in indicator.toolTip()
|
||||
assert indicator.sizeHint().height() == idle_height
|
||||
|
||||
indicator.set_connected(False)
|
||||
assert indicator.icon().isNull()
|
||||
|
||||
|
||||
class TestTransportLayout:
|
||||
def test_cast_indicator_does_not_move_the_volume_slider(
|
||||
self, qapp, tmp_path):
|
||||
def test_casting_leaves_the_bar_layout_alone(self, qapp, tmp_path):
|
||||
from lintunes.gui.transport import BAR_HEIGHT, TransportBar
|
||||
from lintunes.preferences import Preferences
|
||||
|
||||
@@ -901,14 +876,40 @@ class TestTransportLayout:
|
||||
bar.show()
|
||||
idle_y = bar._volume_slider.pos().y()
|
||||
|
||||
bar._cast_indicator.set_connected(True, "Kitchen")
|
||||
# The visualizer panel doubles as the cast indicator, so connecting
|
||||
# adds no widget and nothing moves.
|
||||
player.set_sink(_FakeSink())
|
||||
bar.layout().activate()
|
||||
|
||||
assert bar._volume_slider.pos().y() == idle_y
|
||||
# The whole column has to stay inside the bar's documented height.
|
||||
assert bar.sizeHint().height() <= BAR_HEIGHT
|
||||
bar.hide()
|
||||
|
||||
def test_clicking_the_visualizer_while_casting_disconnects(
|
||||
self, qapp, tmp_path):
|
||||
from lintunes.gui.transport import TransportBar
|
||||
from lintunes.preferences import Preferences
|
||||
|
||||
player = _player(qapp, [])
|
||||
cast = MagicMock()
|
||||
bar = TransportBar(player, MagicMock(), Preferences(tmp_path),
|
||||
cast=cast)
|
||||
vis = bar._visualizer
|
||||
before = vis._mode
|
||||
|
||||
# Not casting: a click still cycles the brightness as it always did.
|
||||
vis.mousePressEvent(None)
|
||||
assert vis._mode != before
|
||||
cast.disconnect.assert_not_called()
|
||||
|
||||
player.set_sink(_FakeSink())
|
||||
mode_while_casting = vis._mode
|
||||
vis.mousePressEvent(None)
|
||||
|
||||
cast.disconnect.assert_called_once()
|
||||
# Brightness must not cycle — it means nothing when there are no bars.
|
||||
assert vis._mode == mode_while_casting
|
||||
|
||||
|
||||
class TestCastDialog:
|
||||
def test_devices_appear_and_disappear(self, qapp):
|
||||
@@ -966,13 +967,24 @@ class TestSpinner:
|
||||
|
||||
|
||||
class TestCastIcons:
|
||||
def test_cast_glyphs_render_and_differ(self, qapp):
|
||||
def test_cast_glyph_renders(self, qapp):
|
||||
from PyQt6.QtGui import QColor
|
||||
idle = transport_icon("cast", QColor("#4A4A4A"))
|
||||
live = transport_icon("cast_connected", QColor("#4A4A4A"))
|
||||
assert not idle.isNull() and not live.isNull()
|
||||
assert (idle.pixmap(20, 20).toImage()
|
||||
!= live.pixmap(20, 20).toImage())
|
||||
assert not transport_icon("cast", QColor("#4A4A4A")).isNull()
|
||||
|
||||
def test_a_bigger_glyph_is_repainted_not_upscaled(self, qapp):
|
||||
from PyQt6.QtGui import QColor
|
||||
big = transport_icon("cast", QColor("#4A4A4A"), size=40)
|
||||
# Rendered at the requested size rather than a stretched 20px pixmap,
|
||||
# so the panel-sized glyph stays crisp.
|
||||
assert big.pixmap(40, 40).size().width() == 40
|
||||
assert big.availableSizes()[0].width() == 40
|
||||
|
||||
def test_size_is_part_of_the_cache_key(self, qapp):
|
||||
from PyQt6.QtGui import QColor
|
||||
small = transport_icon("cast", QColor("#4A4A4A"), size=20)
|
||||
big = transport_icon("cast", QColor("#4A4A4A"), size=40)
|
||||
assert small.availableSizes()[0].width() == 20
|
||||
assert big.availableSizes()[0].width() == 40
|
||||
|
||||
|
||||
class TestVisualizerWhileCasting:
|
||||
@@ -992,3 +1004,17 @@ class TestVisualizerWhileCasting:
|
||||
|
||||
player.set_sink(None)
|
||||
assert vis._no_pcm is False
|
||||
|
||||
def test_names_the_device_it_is_casting_to(self, qapp, tmp_path):
|
||||
from lintunes.gui.visualizer import VisualizerWidget
|
||||
from lintunes.preferences import Preferences
|
||||
|
||||
player = _player(qapp, [])
|
||||
vis = VisualizerWidget(player, Preferences(tmp_path))
|
||||
|
||||
player.set_sink(_FakeSink()) # _FakeSink.name == "Kitchen"
|
||||
assert "Kitchen" in vis.toolTip()
|
||||
assert "click to stop" in vis.toolTip()
|
||||
|
||||
player.set_sink(None)
|
||||
assert "cycle" in vis.toolTip() # back to the brightness control
|
||||
|
||||
Reference in New Issue
Block a user