"""Round 79: per-track volume, Get Info tidy-up, Preferences headings, and the laser rig diagram. 1. A track's volume adjustment (iTunes' "Volume Adjustment", already imported) scales the output while that track plays. 2. Get Info edits it — one track or several — and has no dead Previous/Next buttons. 3. Preferences section headings are bold and 2 px larger than the text. 4. The laser page draws the rig, labelling distance, angles and dot size in place of those sliders' own numbers. """ import pytest from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtGui import QFontInfo from PyQt6.QtWidgets import QApplication, QLabel, QPushButton from lintunes.player import volume_with_adjustment, VOLUME_ADJUSTMENT_MAX from lintunes.preferences import Preferences from tests.test_round29 import _player, _local_track, _FakeSink # -------------------------------------------------------------------------- # 1. The player # -------------------------------------------------------------------------- class TestVolumeAdjustment: def test_none_changes_nothing(self): assert volume_with_adjustment(0.6, 0) == 0.6 def test_minus_100_is_silence_and_plus_100_doubles(self): assert volume_with_adjustment(0.6, -VOLUME_ADJUSTMENT_MAX) == 0.0 assert volume_with_adjustment(0.3, VOLUME_ADJUSTMENT_MAX) == 0.6 def test_never_past_full(self): assert volume_with_adjustment(0.8, VOLUME_ADJUSTMENT_MAX) == 1.0 def test_out_of_range_values_are_clamped(self): assert volume_with_adjustment(0.5, -10_000) == 0.0 assert volume_with_adjustment(0.5, 10_000) == 1.0 def test_applied_when_the_track_loads(self, qapp, tmp_path): quiet = _local_track(tmp_path, 1, volume_adjustment=-128) plain = _local_track(tmp_path, 2) player = _player(qapp, [quiet, plain]) sink = _FakeSink() player.set_sink(sink) player.set_volume(0.8) player.play_queue([1, 2], 0) assert sink.volume == pytest.approx(0.8 * (1 - 128 / 255)) assert player.volume() == 0.8 # the slider is untouched player.next() assert sink.volume == pytest.approx(0.8) def test_moving_the_slider_keeps_the_adjustment(self, qapp, tmp_path): track = _local_track(tmp_path, 1, volume_adjustment=255) player = _player(qapp, [track]) sink = _FakeSink() player.set_sink(sink) player.play_queue([1], 0) player.set_volume(0.25) assert sink.volume == pytest.approx(0.5) def test_an_edit_while_playing_applies_on_refresh(self, qapp, tmp_path): track = _local_track(tmp_path, 1) player = _player(qapp, [track]) sink = _FakeSink() player.set_sink(sink) player.set_volume(0.6) player.play_queue([1], 0) track.volume_adjustment = -255 player.refresh_volume() assert sink.volume == 0.0 # -------------------------------------------------------------------------- # 2. Get Info # -------------------------------------------------------------------------- @pytest.fixture def manager(tmp_path): from lintunes.library_manager import LibraryManager from lintunes.models import Library, Track library = Library() for tid, adj in ((1, 0), (2, 51), (3, 51)): library.tracks[tid] = Track(track_id=tid, name=f"Song {tid}", volume_adjustment=adj) return LibraryManager(library, tmp_path) def _info(manager, ids): from lintunes.gui.info_dialog import InfoDialog return InfoDialog(manager, ids) class TestInfoDialog: def test_no_previous_or_next(self, qapp, manager): for ids in ([1], [1, 2]): texts = {b.text() for b in _info(manager, ids).findChildren(QPushButton)} assert not texts & {"Previous", "Next"} def test_shows_the_tracks_adjustment(self, qapp, manager): dialog = _info(manager, [2]) assert dialog._volume.value() == 20 # 51/255 assert dialog._volume_label.text() == "+20%" assert _info(manager, [1])._volume_label.text() == "None" def test_saving_one_track(self, qapp, manager): dialog = _info(manager, [1]) dialog._volume.setValue(-50) dialog._on_ok() assert manager.library.tracks[1].volume_adjustment == -128 def test_an_untouched_value_keeps_its_exact_number(self, qapp, manager): manager.library.tracks[1].volume_adjustment = 37 # an iTunes value dialog = _info(manager, [1]) dialog._on_ok() assert manager.library.tracks[1].volume_adjustment == 37 def test_the_edit_is_undoable(self, qapp, manager): dialog = _info(manager, [1]) dialog._volume.setValue(100) dialog._on_ok() manager.undo_stack.undo() assert manager.library.tracks[1].volume_adjustment == 0 def test_several_tracks_mixed_untouched(self, qapp, manager): dialog = _info(manager, [1, 2]) assert dialog._volume_label.text() == "Mixed" dialog._on_ok() assert manager.library.tracks[1].volume_adjustment == 0 assert manager.library.tracks[2].volume_adjustment == 51 def test_several_tracks_shared_value(self, qapp, manager): assert _info(manager, [2, 3])._volume_label.text() == "+20%" def test_several_tracks_touched(self, qapp, manager): dialog = _info(manager, [1, 2]) dialog._volume.setValue(-100) dialog._on_ok() assert manager.library.tracks[1].volume_adjustment == -255 assert manager.library.tracks[2].volume_adjustment == -255 # -------------------------------------------------------------------------- # 3 & 4. Preferences # -------------------------------------------------------------------------- class _LastFm(QObject): login_finished = pyqtSignal(bool, str) status_message = pyqtSignal(str) def is_logged_in(self): return False def _prefs_dialog(prefs): from lintunes.gui.preferences_dialog import PreferencesDialog return PreferencesDialog(prefs, _LastFm()) class TestHeadings: def test_bold_and_two_px_larger(self, qapp, tmp_path): from lintunes import theme prefs = Preferences(tmp_path) theme.apply_theme(QApplication.instance(), prefs) # as the app runs dialog = _prefs_dialog(prefs) dialog.show() text_px = QFontInfo(QApplication.font()).pixelSize() headings = dialog.findChildren(QLabel, "prefs-heading") assert {h.text() for h in headings} >= {"Interface", "Visualizer", "Colors", "Last.fm"} for heading in headings: assert heading.font().bold() assert QFontInfo(heading.font()).pixelSize() == text_px + 2 dialog.close() def test_the_boxes_contents_stay_plain(self, qapp, tmp_path): dialog = _prefs_dialog(Preferences(tmp_path)) assert not dialog._glow.font().bold() class TestLaserDiagram: def test_the_dialog_has_one_and_it_follows_the_settings(self, qapp, tmp_path): prefs = Preferences(tmp_path) dialog = _prefs_dialog(prefs) dialog._angle_sliders["visualizer_laser_pitch"].setValue(-25) dialog._distance_slider.setValue(16) config = dialog._diagram._config assert config["visualizer_laser_pitch"] == -25 assert config["visualizer_laser_distance"] == 1.6 def test_labelled_sliders_lost_their_numbers(self, qapp, tmp_path): dialog = _prefs_dialog(Preferences(tmp_path)) page = dialog._viz_pages.widget(2) dialog._viz_pages.setCurrentIndex(2) dialog.show() visible = {label.text() for label in page.findChildren(QLabel) if label.isVisible()} assert "3.5 m" not in visible and "0°" not in visible assert "3 px" not in visible dialog.close() def test_paints_at_any_setting(self, qapp): from lintunes.gui import visualizer_styles as vs from lintunes.gui.laser_diagram import LaserDiagram for overrides in ({}, {"visualizer_laser_pitch": 60, "visualizer_laser_yaw": -60, "visualizer_laser_roll": 180, "visualizer_laser_distance": 1.0, "visualizer_laser_dot": 16}): config = vs.settings(None) config.update(overrides) diagram = LaserDiagram(config) diagram.resize(420, 160) assert not diagram.grab().isNull() def test_the_screen_turns_with_the_angles(self, qapp): from PyQt6.QtCore import QPointF from lintunes.gui import visualizer_styles as vs from lintunes.gui.laser_diagram import LaserDiagram diagram = LaserDiagram(vs.settings(None)) square = diagram.screen_corners(QPointF(0, 0), 30) pitched = diagram.screen_corners(QPointF(0, 0), 30, pitch=40, yaw=0, roll=0) # Tipping the top away moves the top edge along the beam. assert pitched[2].x() != pytest.approx(square[2].x()) # A square screen's vertical edges are vertical. assert square[1].x() == pytest.approx(square[2].x()) # -------------------------------------------------------------------------- # 5. Copying songs gives other apps a song list # -------------------------------------------------------------------------- def _table(tracks, playlist_mode=True): from lintunes.gui.track_table import TrackTableView table = TrackTableView(playlist_mode=playlist_mode) table.set_source_playlist("PID") table.set_tracks(tracks) return table def _songs(): from lintunes.models import Track return [Track(track_id=1, name="Low Tide", artist="The Wheelhouse"), Track(track_id=2, name="Café Noir", artist="Marguerite Vaux"), Track(track_id=3, name="Orphan Take", artist="")] class TestCopyAsText: def test_select_all_copy_pastes_as_a_list(self, qapp): from lintunes.gui.track_table import parse_tracks_mime table = _table(_songs()) table.selectAll() table.copy_selection() mime = QApplication.clipboard().mimeData() assert mime.text() == ("The Wheelhouse - Low Tide\n" "Marguerite Vaux - Café Noir\n" "Orphan Take") # …and pasting into a playlist still reads the songs themselves. assert parse_tracks_mime(mime)["track_ids"] == [1, 2, 3] def test_the_list_follows_the_order_shown(self, qapp): from PyQt6.QtCore import Qt table = _table(_songs(), playlist_mode=False) name = table.model_.fields.index("name") table.sortByColumn(name, Qt.SortOrder.AscendingOrder) table.selectAll() table.copy_selection() assert QApplication.clipboard().text().splitlines() == [ "Marguerite Vaux - Café Noir", "The Wheelhouse - Low Tide", "Orphan Take"] def test_cut_carries_the_list_too(self, qapp): table = _table(_songs()) table.selectRow(0) table.cut_selection() assert QApplication.clipboard().text() == "The Wheelhouse - Low Tide" def test_untitled_song(self, qapp): from lintunes.gui.track_table import song_line from lintunes.models import Track assert song_line(Track(track_id=9, name="", artist="X")) == "X - Untitled"