"""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()