Ctrl+C (and Cut) on selected songs now also puts plain text on the clipboard — one "Artist - Title" per line, in the order the table shows — so a whole playlist pastes into a message or a document. Pasting back into a playlist still reads the songs themselves, as before.
285 lines
11 KiB
Python
285 lines
11 KiB
Python
"""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"
|