The cones move only on the plugin's own 25 fps frames again: driving them from fresh audio every refresh made bass-heavy songs twitch (a jump on 84% of refreshes against 41%). The continuous turn, texture crossfade and the GPU big window stay. The tile: a click opens it big (and it stays blank while big); hovering shows a colour wheel (colour / gray / off) and up/down arrows (previous / next visualizer). No more double-click or click-to-cycle. The big album art closes on a click or Esc, like the big visualizer.
192 lines
7.2 KiB
Python
192 lines
7.2 KiB
Python
"""Round 86: Kaleidostrobe smooth again, and the visualizer tile's controls.
|
|
|
|
1. The cones move only on the plugin's own 25 fps frames (driving them from
|
|
fresh audio at every refresh, Round 85, made bass-heavy songs twitch).
|
|
2. The tile: hovering shows a colour wheel (colour / gray / off) and ▲/▼
|
|
(style); a click anywhere else opens it big; no double-click any more.
|
|
3. While it's big, the tile stays blank.
|
|
4. The big album art closes on a click, like the big visualizer.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
|
|
from PyQt6.QtCore import QEvent, QObject, QPointF, Qt, pyqtSignal
|
|
from PyQt6.QtGui import QColor, QImage, QKeyEvent, QMouseEvent, QPainter
|
|
|
|
from lintunes.gui import visualizer_styles as vs
|
|
from lintunes.gui.visualizer import MODE_DIM, MODE_OFF, MODE_ON, VisualizerWidget
|
|
from lintunes.gui.visualizer_kaleido import KaleidostrobeRenderer
|
|
from lintunes.preferences import Preferences
|
|
|
|
from test_round84 import _frame
|
|
|
|
|
|
class _FakePlayer(QObject):
|
|
audio_buffer = pyqtSignal(object)
|
|
playing_changed = pyqtSignal(bool)
|
|
track_changed = pyqtSignal(object)
|
|
|
|
def is_playing(self):
|
|
return False
|
|
|
|
|
|
def _click(widget, pos, button=Qt.MouseButton.LeftButton):
|
|
pos = QPointF(pos)
|
|
for kind, buttons in ((QEvent.Type.MouseButtonPress, button),
|
|
(QEvent.Type.MouseButtonRelease, Qt.MouseButton.NoButton)):
|
|
event = QMouseEvent(kind, pos, pos, button, buttons,
|
|
Qt.KeyboardModifier.NoModifier)
|
|
if kind == QEvent.Type.MouseButtonPress:
|
|
widget.mousePressEvent(event)
|
|
else:
|
|
widget.mouseReleaseEvent(event)
|
|
|
|
|
|
def _tile(tmp_path, style="bars"):
|
|
prefs = Preferences(tmp_path)
|
|
prefs.set("visualizer_style", style)
|
|
vis = VisualizerWidget(_FakePlayer(), prefs)
|
|
vis.resize(140, 62)
|
|
return prefs, vis
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 1. Smooth again
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestSmooth:
|
|
def test_the_cone_moves_only_on_the_plugins_frames(self, qapp):
|
|
"""Height and depth are the plugin's own: driving them from fresh
|
|
audio every refresh made the picture twitch."""
|
|
r = KaleidostrobeRenderer()
|
|
r.advance(_frame(dt=0.04))
|
|
seen = []
|
|
for i in range(30):
|
|
r.advance(_frame(dt=1 / 60, hz=55 + 40 * (i % 3)))
|
|
apex = r._shown[0][0].triangles[0].xyz[0][2]
|
|
seen.append((r._engine.frame_count, apex))
|
|
for (fa, za), (fb, zb) in zip(seen, seen[1:]):
|
|
if fa == fb:
|
|
assert za == zb
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 2. The tile's controls
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestTile:
|
|
def test_a_click_in_the_middle_opens_big(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path)
|
|
_click(vis, vis.rect().center())
|
|
try:
|
|
assert vis._big is not None and vis._big.isVisible()
|
|
assert vis._mode == MODE_ON # brightness untouched
|
|
finally:
|
|
vis._big.close()
|
|
|
|
def test_the_colour_wheel_cycles_and_does_not_open(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path)
|
|
wheel = vis._button_rects()["mode"].center()
|
|
seen = []
|
|
for _ in range(3):
|
|
_click(vis, wheel)
|
|
seen.append(vis._mode)
|
|
assert seen == [MODE_DIM, MODE_OFF, MODE_ON]
|
|
assert prefs.get("visualizer_mode") == MODE_ON
|
|
assert vis._big is None
|
|
|
|
def test_arrows_step_through_every_style_and_wrap(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path, "bars")
|
|
down = vis._button_rects()["next"].center()
|
|
up = vis._button_rects()["prev"].center()
|
|
seen = []
|
|
for _ in range(len(vs.STYLES)):
|
|
_click(vis, down)
|
|
seen.append(prefs.get("visualizer_style"))
|
|
assert seen == list(vs.STYLES[1:]) + [vs.STYLES[0]]
|
|
_click(vis, up) # bars → the last one
|
|
assert prefs.get("visualizer_style") == vs.STYLES[-1]
|
|
assert isinstance(vis._renderer, KaleidostrobeRenderer)
|
|
assert vis._big is None
|
|
|
|
def test_a_double_click_does_nothing_special(self, qapp, tmp_path):
|
|
assert "mouseDoubleClickEvent" not in VisualizerWidget.__dict__
|
|
|
|
def test_controls_show_only_on_hover(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path)
|
|
prefs.set("visualizer_mode", MODE_OFF)
|
|
|
|
def shot():
|
|
return vis.grab().toImage().pixel(int(vis._button_rects()["mode"].center().x()),
|
|
int(vis._button_rects()["mode"].center().y()))
|
|
plain = shot()
|
|
vis._hovering = True
|
|
assert shot() != plain
|
|
vis._hovering = False
|
|
assert shot() == plain
|
|
|
|
def test_tooltips_say_what_each_part_does(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path, "bars")
|
|
vis._hot = "next"
|
|
vis._update_tooltip()
|
|
assert "Radial dots" in vis.toolTip()
|
|
vis._hot = "mode"
|
|
vis._update_tooltip()
|
|
assert "colour" in vis.toolTip()
|
|
vis._hot = None
|
|
vis._update_tooltip()
|
|
assert "big" in vis.toolTip()
|
|
|
|
def test_casting_click_still_stops_casting(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path)
|
|
stopped = MagicMock()
|
|
vis.cast_stop_requested.connect(stopped)
|
|
vis._no_pcm = True
|
|
_click(vis, vis._button_rects()["mode"].center())
|
|
stopped.assert_called_once()
|
|
assert vis._mode == MODE_ON
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 3. Blank while big
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestBlankWhileBig:
|
|
def test_tile_draws_nothing_while_big_is_open(self, qapp, tmp_path):
|
|
prefs, vis = _tile(tmp_path)
|
|
vis._renderer = MagicMock(spec=["paint", "advance", "clear", "name"])
|
|
img = QImage(140, 62, QImage.Format.Format_ARGB32)
|
|
painter = QPainter(img)
|
|
vis.paint_renderer(painter, vis.rect())
|
|
assert vis._renderer.paint.call_count == 1
|
|
vis.open_big()
|
|
try:
|
|
vis.paint_renderer(painter, vis.rect()) # the tile
|
|
assert vis._renderer.paint.call_count == 1
|
|
vis.paint_renderer(painter, vis.rect(), big=True) # the window
|
|
assert vis._renderer.paint.call_count == 2
|
|
finally:
|
|
painter.end()
|
|
vis._big.close()
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 4. Big album art
|
|
# --------------------------------------------------------------------------
|
|
|
|
class TestArtWindow:
|
|
def test_a_click_closes_it(self, qapp):
|
|
from lintunes.gui.art_window import ArtWindow
|
|
win = ArtWindow()
|
|
win.resize(300, 300)
|
|
win.show()
|
|
_click(win, win.contentsRect().center())
|
|
assert not win.isVisible()
|
|
|
|
def test_escape_closes_it(self, qapp):
|
|
from lintunes.gui.art_window import ArtWindow
|
|
win = ArtWindow()
|
|
win.show()
|
|
win.keyPressEvent(QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Escape,
|
|
Qt.KeyboardModifier.NoModifier))
|
|
assert not win.isVisible()
|